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
__csrfhidden field matching the__verve_csrfHttpOnly cookie —ctx.actionFormwires this for you; addctx.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
- Never interpolate request data into
raw/scriptInline. - Validate URLs from users with
sanitizeUrlbefore storing or rendering (the bookmarks example rejects unsafe URLs server-side). - Keep Actions free of per-request allocators — return static or threadlocal data, and guard shared state for multi-threaded dispatch.
Next: Fetch.