Skip to content

Documentation

Security Documentation

Security reference map and the internal self-assessment. The August 2026 checklist re-checks to ~99.9/100 (99.93). A third-party penetration test is still open.

10/11/2026
12 min read Edit on GitHub

This page is the map of the security reference and the internal self-assessment of the controls in the current source. The weighted score was checked against that source on 2026-09-29. Inventory rows added on 2026-10-11 describe controls that shipped after that check. They were not given a new weight, so the score below stays 99.93.

Important

The ~99.9% figure below is this repository’s own weighted checklist. It is not a penetration test. One item stays on the open list: a dated penetration test by someone outside the project. Still open →

Self-assessment

The self-assessment has two parts, both dated 2026-09-29: the weighted checklist, and an inventory of what the source does today. An inventory row leaves the open list when the outstanding work is in the tree and a regression test fails if that control is removed. The penetration-test row leaves the list when a dated third-party report exists. That report is not one of the six weights, so a clean scanner run cannot turn the checklist into an external test.

Internal scorecard

Same weights as the August 2026 checklist. Re-checked on 2026-09-29. bun run test:security passed 19 files / 343 tests. bun run risk:audit passed 5/5: the security-risk scan reported 0 findings over 2,846 files, the secret-misuse scan and the slop scanner reported 0 errors, bun audit reported no vulnerabilities in 718 packages, and OSV.dev reported 0 affected of 764 packages.

Dimension Weight Score This check
CVE track record 25% 100 No published advisory in this run’s bun audit or OSV.dev query.
Cryptography 15% 100 AES-256-GCM, Argon2id, HMAC-SHA-256. The algorithms were not re-derived by the scanners; they are the ones in src/utils/security/crypto.ts.
Disclosure & response 10% 100 security.txt, the reply windows in SECURITY.md, and GitHub Security Advisories. This run did not send a test report to the inbox.
Auth & session 20% 99.8 User-agent step-up, WebAuthn origin and user-presence checks, and the SSO metadata cache are in the tree with regression tests. Back-channel logout still walks only this process’s sessions. The 0.2 is the residual the August row already carried. This re-check does not invent a higher score.
Input validation 15% 99.8 private and hidden fields are redacted on the way out of a read. A warm collection GET runs wafGuard before cache or database. The 0.2 stays for the same reason.
Dependency hygiene 15% 100 bun audit and OSV.dev, as cited above.

Weighted: 25.0 + 15.0 + 10.0 + 19.96 + 14.97 + 15.00 = 99.93/100, which rounds to ~99.9%.

The August footnotes that are now in the tree (private-field reads, the warm-read threat scan, user-agent step-up, WebAuthn client-data verification, the SSO metadata cache) did not move the arithmetic. The rubric never assigned them a point value of their own, and the two 99.8 rows were not raised.

How close this is to a penetration test

A penetration test is a dated report by someone outside the project who attacks a running deployment. This repository does not contain that report. The in-repo checks fail when a named control is removed: 343 security regressions, plus the five scanners in bun run risk:audit. They do not search for a bug the checklist never named. The OWASP matrix maps controls to chapters. It is not a passed ASVS audit and not a certificate. The remaining distance is the external engagement itself, which is the open row below and the roadmap item “Third-Party Security Audit”.

Still open

Item In the current tree Still outstanding
Third-party penetration test Advisory-database scans and the in-repo scanners (bun run risk:audit, bun run test:security) cover the cases they contain. A dated penetration test by someone outside the project.

In the current tree

Area What the source does Where
Authorization Unmapped /api routes fail closed. ENDPOINT_PERMISSIONS plus hasPermissionWithRoles is the authorization point. The unwired Policy-as-Code engine is gone. src/routes/api/[...path]/+server.ts, WAF and middleware
Request lanes Writes run handleSecurity, rate limit, CSRF, authentication, and authorization. API reads run handleSecurity, authentication, and authorization, and skip the mutation rate limiter. Page routes run the full sequence. Page loads then use getAuthenticatedUser and requirePagePermission. src/hooks.server.ts
Field access permissions.visibility of private or hidden is a guard even when the role lists are empty. Non-admins are redacted on SDK reads. Writes of a guarded field return 403. The response cache keeps the full row. Filter and sort drop a guarded name the caller may not read. Admins, system callers, and schemas with no guarded fields keep the same query object. A keyset cursor on a blocked field returns 400. Field-level access control, src/utils/guarded-query.ts
Row ownership When a collection sets ownership: { field }, list, count, and stream stay scoped to that field. Update, delete, and bulk delete return 404 both for a missing row and for a row the caller does not own. A caller with no user id matches nothing. Admins and system writes skip the extra read. A schema with no ownership adds no query. src/services/sdk/namespaces/collections-namespace.ts, Schema model
Scheduled publish A status-transition job stores the authenticated user. The client cannot set the actor or the tenant. The worker and the schedule sweep run the publication gate as that user. A job or scheduled entry with no actor is left unpublished. src/services/scheduler.ts, src/services/background/jobs/scheduled-jobs.ts
CSV export A text cell that starts with =, +, -, @, tab, or carriage return is prefixed with ' on the way out. Numbers and booleans are unchanged. src/utils/export-encode.ts, Collection export
Media list bounds Media list limit uses the shared page-size ceiling (200). Sort accepts size, filename, updatedAt, createdAt, and mimeType. Anything else stays updatedAt. There is no size index. src/routes/api/[...path]/handlers/media.ts
WebSocket origin An upgrade whose Origin host differs from the request host is rejected. An empty Origin is allowed. A malformed Origin is rejected. The check runs before the test-secret path. src/hooks.ws.ts, API security
Webhook audit Creating, updating, or deleting a webhook writes WEBHOOK_CREATED, WEBHOOK_UPDATED, or WEBHOOK_DELETED. A failed audit does not fail the request. GET and the test send are not audited. src/routes/api/[...path]/handlers/system.ts
Passwords and sessions Argon2id at 64 MiB, time cost 3, parallelism 4. The 1 MiB / time cost 1 parameters apply when NODE_ENV=test, VITEST=true, or BUN_TEST=true. Sessions rotate every 15 minutes. SESSION_TTL_HOURS defaults to 24. Idle timeout follows SESSION_IDLE_HOURS (0 disables it). SESSION_MAX_PER_USER of 0 means no cap. Authentication, src/utils/security/crypto.ts
Session cookie httpOnly. On a secure context the name is __Host-auth_sessions, Secure, and SameSite=Strict. Otherwise the name is auth_sessions and SameSite=Lax. src/databases/auth/constants.ts
Session cache The turbo auth cache TTL is 60 seconds and slides on every hit (15 minutes when BENCHMARK=true). Credential fields are stripped before a user enters the session cache. src/hooks/handle-turbo-get.ts, src/databases/auth/session-user.ts
Session risk A user-agent change on a live session steps mutations up (403 SESSION_RISK_STEP_UP on /api, redirect to /login?reason=step-up on pages). GET, HEAD, and OPTIONS stay available. /api/user/sessions/reauth, /api/auth/login, /api/auth/logout, and /login stay reachable. A successful password reauth adopts the current browser in memory, on the turbo context, and in the session cache. An IP-only change is logged once per session per hour. Empty stored values stay allowed. The collection write lane applies the same step-up before persist. src/databases/auth/session-user.ts, src/hooks/handle-authorization.ts, src/hooks/handle-collection-write-lane.ts
Passkeys Register and sign-in compare the ceremony origin with the request origin and require the authenticator user-presence flag. A foreign origin is rejected before the signature is accepted. The assertion signature covers the raw client data bytes. Register, sign-in, and revoke are in the tree. src/databases/auth/webauthn/webauthn-service.ts, src/routes/login/auth.remote.ts
SSO session metadata OIDC login writes provider metadata to process memory and to the session cache (sso:session:{id}, 7 days) so another node can finish RP-initiated logout. Back-channel logout still walks the sessions this process has in memory. src/databases/auth/sso-session.ts
Lockout Five failed logins lock the account for 15 minutes. PASSWORD_MIN_LENGTH defaults to 8. src/databases/auth/index.ts
CSRF Mutations on the API dispatcher, GraphQL, and the collection write lane call validateCsrfForRequest. A same-origin Origin or Referer matches the host and skips the token. API-key and API-token users, the Stripe webhook, and public plugin routes skip the dispatcher check. src/utils/security/csrf-utils.ts
Rate limits Tier multipliers are admin 10, staff 2, guest 2, anonymous 1. Pressure scaling leaves the admin tier at 1.0. src/utils/rate-limit/adaptive.ts
Heap shedding handleSecurity rejects non-GET mutations with 503 when the heap ratio exceeds 0.95. /api/system and /setup stay available. Test mode skips the shed. src/hooks/handle-security.ts
WAF handleSecurity, the collection write lane, and the collection read lane call wafGuard.inspectEvent on the path, query, and headers. The read lane does this before any cache or database read. Heap shedding and the Redis WAF limiter stay on handleSecurity. The scanner is a linear walk in threat-scan.ts. Honeypot prefixes (/wp-, /xmlrpc.php, /.env, /.git/, /adminer.php, /phpinfo.php, /actuator/) return an empty 200. Known scanner and AI-crawler user agents from a non-local client get the same empty 200. Localhost is exempt from that user-agent response. WAF and middleware, Server hooks
GraphQL The route rejects depth above 8 and more than 15 aliases. Introspection is blocked when NODE_ENV=production, DEMO_MODE=true, or BLOCK_GRAPHQL_INTROSPECTION=true. handleSecurity also applies a brace-depth pre-filter at 12. src/routes/api/graphql/+server.ts
Headers Pages send Cross-Origin-Embedder-Policy: require-corp. API routes send require-corp, except /api/media/ and paths containing /mediagallery, which send credentialless. The non-production GraphQL playground (ALLOW_GRAPHQL_PLAYGROUND=false turns it off) sends unsafe-none and a CSP that allows the playground scripts. Other API responses use script-src 'self'. Content Security Policy, src/hooks/handle-security-headers.ts
Identifiers Primary keys are RFC 9562 UUIDv7. src/databases/core/id-contract.ts
Encryption AES-256-GCM for field envelopes and the shared crypto module. Field-level encryption, Cryptography
Audit chain AUDIT_CHAIN_SYNC=true runs the hash chain on the write. When the environment variable is unset, the database setting applies, and the fallback is asynchronous. src/utils/security/audit-flags.ts
Testing API NODE_ENV=production returns 403. Other environments need TEST_MODE, VITE_TEST_MODE, or PLAYWRIGHT_TEST, plus a timing-safe x-test-secret. A missing or wrong secret returns 401. BENCHMARK does not open the route. Production builds strip handlers/testing and handle-test-isolation. src/utils/test-bypass.server.ts, vite.config.ts
Body limit A declared Content-Length above 15 MiB is rejected with 413 before the body is read. src/utils/api-body-limits.ts
Disclosure Reports go to security@sveltycms.com or a GitHub Security Advisory. SECURITY.md states a reply within 48 hours and a critical fix within 7 days (high 30 days, medium and low 90 days). Incident response

How a request is checked

classifyRequest in src/hooks.server.ts picks the pipeline after setup is complete.

  1. Collection read lane. A simple collection GET or HEAD with a warm session can be answered by tryCollectionReadLane before the pipeline. The lane scans the path, query, and headers with wafGuard before any cache or database read. Heap shedding and the Redis WAF limiter stay on handleSecurity.
  2. Collection write lane. Opt-in with SVELTY_FAST_LANE_WRITE=1. It runs the WAF guard, CSRF, and the session-risk step-up, then one persist. A cold session falls through.
  3. API write pipeline. handleSecurity, rate limit, system state, compression, authentication, authorization, audit logging, API dispatch.
  4. API read pipeline. handleSecurity, system state, turbo GET, compression, authentication, authorization, API dispatch, token resolution. Rate limit and audit logging are omitted because those hooks cover mutations.
  5. Page pipeline. The read pipeline plus the mutation rate limiter, redirects, user preferences, and audit logging.

Security headers are applied to responses, including errors. Fast static paths such as /favicon.ico return before the pipeline.

Documentation map

Every page in this folder:

Standards and operations

Authentication and access

Cryptography and content

Checks you can run

These commands exercise the tree. The pass counts in the scorecard are from the 2026-09-29 run on this tree.

Command What it runs
bun run risk:audit Secret misuse, slop, security-risk scan, bun audit, OSV.dev
bun run test:security Hooks, authorization, media, GraphQL, the field guard, the read-lane threat scan, write-lane step-up, session risk, the WebAuthn ceremony, and the SSO metadata cache

Deployment checklist

The application ships the controls in the table above. These items are set in the environment that runs it.

  • JWT_SECRET_KEY and ENCRYPTION_KEY are high-entropy values generated at setup (ENCRYPTION_KEY is a 32-byte AES-256-GCM key).
  • The site is served over TLS. HSTS is set when the request is HTTPS.
  • The database listens on a private network.
  • The setup-wizard admin password has been replaced.
  • API tokens use the smallest permission set that caller needs.
  • Backup encryption, key rotation, and log retention match the operator’s compliance window. AUDIT_CHAIN_SYNC is the switch for synchronous hash chaining.

Related

Last updated: 2026-09-29

securitydocumentationauthenticationencryptionbest-practices
Was this page helpful?