{"protocol":"clank-doc/1","frameworkVersion":"0.8.0","slug":"security","title":"Security","description":"Clank treats browser input, agent input, URLs, cookies, request bodies, and persisted document data as untrusted. Secure defaults are applied in the framework, but deployment configuration and application authorization remain part of the bo","group":{"id":"security","title":"Security and resilience"},"url":"https://docs.clank.run/docs/security","source":"docs/security.md","headings":["Built in defenses","Rendering","Requests and RPC","Authentication and data","Node and files","Agent actions","Deployment platform","Recommended production setup","CSP nonces","Reverse proxies and Tailscale","Reporting and audit checklist"],"tableOfContents":[{"id":"built-in-defenses","title":"Built-in defenses","level":2},{"id":"rendering","title":"Rendering","level":3},{"id":"requests-and-rpc","title":"Requests and RPC","level":3},{"id":"authentication-and-data","title":"Authentication and data","level":3},{"id":"node-and-files","title":"Node and files","level":3},{"id":"agent-actions","title":"Agent actions","level":3},{"id":"deployment-platform","title":"Deployment platform","level":3},{"id":"recommended-production-setup","title":"Recommended production setup","level":2},{"id":"csp-nonces","title":"CSP nonces","level":2},{"id":"reverse-proxies-and-tailscale","title":"Reverse proxies and Tailscale","level":2},{"id":"reporting-and-audit-checklist","title":"Reporting and audit checklist","level":2}],"markdown":"# Security\n\nClank treats browser input, agent input, URLs, cookies, request bodies, and persisted document data as untrusted. Secure defaults are applied in the framework, but deployment configuration and application authorization remain part of the boundary.\n\n## Built-in defenses\n\n### Rendering\n\n- Text and attributes are escaped during SSR.\n- Serialized state escapes `<`, `>`, `&`, and Unicode line separators.\n- Inline `on*` attributes are rejected case-insensitively.\n- `javascript:`, `vbscript:`, `file:`, non-image `data:`, SVG data images, and `srcdoc` attributes are rejected.\n- Event listeners must be functions and are installed through `addEventListener`.\n- Two-way binding is restricted to `value`, `checked`, `selected`, and `selectedIndex`.\n- `renderDocument({ nonce })` applies a validated CSP nonce to generated boot-state and module script tags.\n\n`dangerouslySetInnerHTML` deliberately bypasses escaping. Use it only with trusted static content or an application-selected sanitizer. Clank does not include an HTML sanitizer because safe policies depend on the tags, attributes, and URL schemes an application intends to allow.\n\nThe TSX transform is a source-to-source compiler, not a data sandbox. It deliberately preserves application-authored JavaScript and TypeScript expressions in generated modules. Compile only trusted project source, never request or database values; execute mutually untrusted generated applications inside the documented runner isolation boundary.\n\n### Requests and RPC\n\n- JSON endpoints require an `application/json` or `+json` content type.\n- Bodies are streamed through hard byte limits and strict UTF-8/JSON decoding.\n- Same-origin and Fetch Metadata checks reject cross-site state changes.\n- Validation errors omit received values so secrets are not reflected.\n- Production 500 responses are generic across request apps, the Node adapter, agent actions, backends, and the deployment platform. Unexpected exception text is available only to private `onError` hooks.\n- Backend cache and live-query keys are partitioned by auth session.\n- Request, live-argument, live-connection, and cache limits are configurable.\n\n### Authentication and data\n\n- Passwords use versioned scrypt hashes with random salts and optional server-only peppering.\n- Session cookies are `HttpOnly`, `SameSite=Strict` by default, `Secure` on HTTPS, and use the `__Host-` prefix when possible.\n- Only SHA-256 token hashes are stored in SQLite; raw session tokens exist only in cookies and the immediate response construction path.\n- Authenticated mutations require a constant-time CSRF-token comparison.\n- Login errors do not reveal whether an account exists or is disabled.\n- Rate limits use trusted adapter identity rather than caller-selected IP headers.\n- Email verification and password recovery tokens are hashed, expiring, and single-use; recovery delivery is removed from the response-timing path, and password reset revokes prior sessions.\n- Email-code MFA challenges are expiring, attempt-bounded, hashed, and single-use.\n- Passkeys require discoverable credentials and begin without account lookup; they verify challenge, origin, RP ID hash, user presence/verification, signature, and atomic signature-counter advancement.\n- Owned tables enforce the current user in SQL reads and writes.\n- Owner IDs also scope live-query invalidation, so one account's private writes do not republish another account's query.\n- Disabling users, role changes, and revoking sessions close associated live streams across same-host processes.\n- Document `ifVersion` checks reject stale writes instead of silently overwriting newer edits.\n- Mutation writes, output validation, revision updates, and journal records share one transaction.\n- Query snapshots and change metadata are immutable at runtime.\n\n### Node and files\n\n- The Node adapter caps headers and bodies and configures header, request, and keep-alive timeouts.\n- Loopback servers allow only loopback Host values by default.\n- `allowedHosts` is available for production and reverse-proxy hostnames.\n- `trustProxy` is off by default.\n- Static paths are URL-decoded, containment-checked, resolved through the filesystem, checked again after symlinks, and deny dotfiles by default.\n- Static responses use MIME types plus `X-Content-Type-Options: nosniff`.\n\n### Agent actions\n\n- Inputs and optional outputs are runtime validated.\n- Authorization runs before the action handler.\n- HTTP calls to write/destructive actions require `x-clank-confirmation: confirmed` when the action policy is `write` or `always`.\n- Semantic inspection omits password, file, hidden, and inaccessible control values.\n- Agent input refuses disabled, read-only, hidden, and file controls; file upload must use an application-defined, validated action.\n- Confirmation is an accident-prevention protocol, not authorization. A caller able to forge the header still needs application authentication and authorization.\n\n### Deployment platform\n\n- CLI login uses short-lived browser-approved device codes; raw access tokens are returned once and stored only as hashes.\n- Local CLI credentials and project links are bounded, structurally validated, URL-canonicalized, owner-only, and atomically replaced; platform responses are time- and byte-bounded before strict UTF-8/JSON decoding.\n- Artifacts and every contained file are SHA-256 verified before exclusive extraction.\n- Paths, links, special files, sensitive dotfiles, sizes, counts, modes, and decompression output are validated.\n- Builds run locally without a shell; uploaded install/build hooks are never executed by the platform.\n- Secrets use AES-256-GCM and values are never returned by the API.\n- SQL migration history is immutable and pending migrations are transactional.\n- Safe migrations cannot modify Clank-managed SQL namespaces.\n- SQLite is integrity-checked and backed up after quiescing the active app.\n- Database and backup paths reject final symbolic links and use private file permissions.\n- Migration, startup, or health failure restores the prior database and process.\n- Code rollback is health-gated; data rollback is narrowly scoped and confirmed.\n- Organization membership, role, project token scope, and project ownership are checked for every release, log, secret, token, domain, backup, audit, and rollback.\n- Account, organization-site, and project-domain quotas are enforced transactionally with their inserts.\n- Encrypted backup manifests and ciphertext are authenticated and verified before restore.\n- Managed ingress uses exact unique hosts, constrained upstreams, bounded bodies/timeouts, hop-header stripping, safe retries, and circuits.\n- Client disconnects abort proxied upstream work and cancel streamed Node responses.\n\nSee [Platform security](platform-security.md) for the runner trust boundary.\n\n## Recommended production setup\n\n```ts\nconst app = createApp()\n  .use(securityHeaders({\n    contentSecurityPolicy: [\n      \"default-src 'self'\",\n      \"script-src 'self'\",\n      \"style-src 'self'\",\n      \"connect-src 'self'\",\n      \"img-src 'self' data:\",\n      \"base-uri 'self'\",\n      \"form-action 'self'\",\n      \"frame-ancestors 'none'\",\n      \"object-src 'none'\",\n    ].join(\"; \"),\n  }))\n  .route(\"*\", \"*\", ({ request }) => runtime.handle(request));\n\nawait serve(app, {\n  hostname: \"0.0.0.0\",\n  allowedHosts: [\"app.example.com\"],\n  maxBodySize: 1024 * 1024,\n});\n```\n\nAlso:\n\n- terminate TLS at the app or a trusted reverse proxy;\n- set `trustProxy: true` only when untrusted clients cannot reach the Node listener directly;\n- set a strong `CLANK_AUTH_PEPPER` through secret management;\n- keep the SQLite file and backups outside static roots with restrictive OS permissions;\n- compile and serve Tailwind CSS locally in production;\n- add a shared rate limiter when running more than one process;\n- configure email verification, recovery, MFA, passkeys, and bot protection to match the application's risk;\n- log internal exceptions through `onError` without returning them to clients;\n- keep Node and Clank patched and back up the database.\n- run one active built-in process supervisor per project/data directory until a remote worker/leader topology is configured;\n- use Docker or stronger isolation for mutually untrusted deployers;\n- supply the platform master key from external secret management;\n- export scheduled backups and pre-release snapshots off-host.\n\n## CSP nonces\n\nGenerate a fresh unpredictable nonce for every HTML response:\n\n```tsx\nconst nonce = crypto.randomUUID().replaceAll(\"-\", \"\");\nconst page = await renderDocument(view, {\n  nonce,\n  state,\n  scripts: [\"/app.js\"],\n});\n\nreturn html(page, {\n  headers: {\n    \"content-security-policy\":\n      `default-src 'self'; script-src 'self' 'nonce-${nonce}'; object-src 'none'`,\n  },\n});\n```\n\nAny inline script supplied through `head` must receive the same `nonce` property. Avoid `unsafe-inline` for scripts.\n\n## Reverse proxies and Tailscale\n\nFor the authenticated Todo behind Tailscale Serve:\n\n```sh\nPORT=4180 \\\nTRUST_PROXY=1 \\\nALLOWED_HOSTS=localhost,127.0.0.1,nearbyserver.example.ts.net \\\nnode examples/auth-todo/server.js\n\ntailscale serve --https=8446 http://127.0.0.1:4180\n```\n\n`TRUST_PROXY=1` is safe here only because the listener remains on loopback and Tailscale is the only proxy that can reach it. `ALLOWED_HOSTS` must contain the public Tailscale DNS name so Host validation succeeds. With forwarded HTTPS enabled, Clank issues the `Secure; __Host-` session cookie.\n\n## Reporting and audit checklist\n\nBefore release, verify:\n\n- anonymous requests cannot call required queries or mutations;\n- two accounts cannot read or mutate each other's owned rows;\n- missing/wrong CSRF tokens fail;\n- cross-origin writes fail;\n- logout and user disabling revoke current live access;\n- malformed paths and oversized bodies return 4xx, not 500;\n- internal exceptions do not appear in production responses;\n- static traversal, dotfile, and symlink escape attempts fail;\n- CSP is present on HTML;\n- cookies are `HttpOnly`, `Secure`, `SameSite`, and host-only in production;\n- the complete app works in two independent browser contexts.\n\nClank's test suite contains executable checks for these invariants. Security is iterative: repeat this review when adding a new transport, credential type, storage backend, raw-HTML path, or deployment topology.\n\nSee the [ASVS-oriented verification map](security-asvs.md), [threat model](threat-model.md), and [chaos drills](chaos-testing.md) for release evidence and residual responsibilities.\n"}