Security

Defaults first: escaped text, sanitized URLs, stripped raw HTML, CSRF on every generated endpoint, and CSP nonces on inline scripts.

Output escaping

Everything that goes through .text(), textNode, or markdown is HTML-escaped by the renderer. The deliberate escape hatches are ctx.raw(bytes) and ctx.scriptInline(body) — anything you pass them is emitted verbatim, so never feed them request-derived data.

URL sanitization

if (verve.sanitizeUrl(user_supplied)) |safe| {
    _ = node.attr("href", safe);
}

Allows http:, https:, mailto:, and relative URLs; returns null for javascript:, data:, and friends. Markdown link/image URLs pass through this automatically.

CSRF

Every /api/<fn> endpoint generated from Actions is protected:

  • Native form posts must carry the __csrf hidden field matching the __verve_csrf HttpOnly cookie — ctx.actionForm wires this for you; add ctx.csrfField() manually to hand-rolled forms.
  • JSON posts (island server-fn calls) pass an Origin-vs-Host check.
  • Tokens are base64url(timestamp ‖ HMAC-SHA256(key, ts)), valid 24h.

Set VERVE_CSRF_KEY (64 hex chars) in the environment for tokens that survive server restarts; otherwise a random key is generated at boot.

CSP nonces

The server stamps a per-request nonce on every ctx.script / ctx.scriptInline / inline style and echoes it in the Content-Security-Policy header. You get this for free by using the context factories; hand-written <script> tags via raw bypass it — don't.

For custom rendering pipelines, verve.setRendererNonce(nonce) sets the nonce the renderer applies.

Practical rules

  1. Never interpolate request data into raw/scriptInline.
  2. Validate URLs from users with sanitizeUrl before storing or rendering (the bookmarks example rejects unsafe URLs server-side).
  3. Keep Actions free of per-request allocators — return static or threadlocal data, and guard shared state for multi-threaded dispatch.

Next: Fetch.