Security model

What is protected, what is yours to handle, and the known weaknesses.

This chapter states what Crisptastic protects, what it leaves to you, and where today's implementation is weaker than the design documents. It describes the code as built.

What the framework gives you

Output escaping

{{ ... }} and {{= ... }} HTML-escape & < > " '. Raw output ({{~ ... }}) exists for trusted content. Escaping is the same for every output format. See Pages and templating.

Response headers

Every response carries X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, X-Frame-Options (DENY by default) and a restrictive Permissions-Policy. You can add a Content-Security-Policy and per-route CORS rules in configuration. There is no automatic CSP; add one.

Cookies

All cookies are HttpOnly, SameSite=Lax and Path=/, and Secure on a production host. This is fixed.

CSRF

On a production host, a POST that carries a session must present the session's CSRF token (_csrf field or X-CSRF-Token header), or it is rejected with 403 before your page runs. Anonymous POSTs are not checked. There is no Origin or Referer validation. dev does not enforce CSRF. See Sessions and forms.

Safe methods stay safe

On a production host, a GET or HEAD request that tries to write a session, set a cookie or issue a non-GET @fetch fails with a 500. This prevents state changes from links and prefetching.

Outbound requests (SSRF)

@fetch and the helper endpoints can reach only https URLs on port 443, never private, loopback, link-local or metadata addresses. DNS answers are validated and the chosen address is pinned for the connection. Redirects are never followed. See Fetching and shaping data.

Request parsing

The server parses HTTP strictly: one Host header, bounded request line, headers, query pairs and body (1 MiB), no Transfer-Encoding, and rejection of paths with .., encoded slashes or control characters.

Secrets and Capsules

  • A Capsule is signed (Ed25519, COSE) and then encrypted (age). The host checks the signature against trusted public keys before it reads the contents.
  • Secure variables are held in locked, dump-excluded, zeroised memory on the host and are never given to templates.
  • The host refuses to run as root, applies umask 077, takes an exclusive lock on its root folder and disables core dumps.
  • Unknown domains fail closed with 421; one site can never receive another site's requests.

What is yours to handle

  • Authentication and authorisation. There is no route-level auth = "required"<code>. A page protects itself by checking</code>context.session and redirecting. Put the check in a partial you include on every protected page.
  • Input validation. Request values arrive as strings. Validate them before using them in @fetch URLs or queries; use num() and explicit checks.
  • Authorisation of integration endpoints. See below.
  • A Content-Security-Policy suited to your site.
  • Not putting private files in the site folder. A deployment packages the whole folder, and the assets folder is served publicly, including raw .scss.

Known weaknesses to design around

These are facts about the current implementation, collected here so you can make informed choices. None is hidden in the code; all are reported by the review notes behind this guide.

  1. Helper endpoints are unauthenticated (Auth0, Stripe, S3, Cloudflare). Any visitor can call them. Examples: anyone can create Stripe Checkout sessions for any price_id or quantity; anyone can obtain presigned S3 upload and download URLs for any key under the configured prefix, with no size or type limit.
  2. Stripe checkout_session_get inserts the supplied session_id into the Stripe URL without encoding it. A crafted value can address other Stripe API paths with your secret key; the response is field-filtered on success, but a failure echoes Stripe's body. Treat this endpoint as unsafe to expose publicly until it is fixed.
  3. Provider secrets for helper endpoints are ordinary strings read from the host process environment. They are not held in the locked secret memory, and all sites on a host share that environment.
  4. [fetch] credentials and headers go to every host a page fetches.
  5. @fetch has no default timeout or response-size limit.
  6. Sessions never expire, are plain JSON files whose names are the session ids, and dev accepts attacker-chosen session ids. Production issues random 32-byte ids.
  7. CSRF is skipped for anonymous requests, so a login form is exposed to login-CSRF.
  8. Auth0 state is not checked by the framework. Your callback page must compare it.
  9. json() does not escape <, so {{~ json(x) }} inside a <script> element can be abused. Use an escaped attribute.
  10. Request headers, including Cookie and Authorization, are visible to templates through request.headers. Do not print the object.
  11. A Capsule contains your secrets in encrypted form. Whoever holds a host identity and the Capsule can read them.
  12. Tenants are not isolated from each other. All sites on a host share one process. There is no hard cancellation or per-tenant limit. Host only sites you trust.

If you operate a site that handles money or personal data, read these before you launch, and read Known gaps.