place

Security

place ships with security defaults that are on by default. The presets are named values, not opt-in arrays of headers to maintain — pick 'standard' or 'strict', and the framework wires CSP, CSRF, same-origin enforcement, body size limits, and prototype-pollution guards in one shot.

Pick a preset

ts
app({ pages: [...], security: 'standard', // 'standard' | 'strict' | 'off' | { ...custom } }).run()

What 'standard' enables

ts
// security: 'standard' enables: // - Content-Security-Policy (strict, no inline scripts/styles) // - X-Content-Type-Options: nosniff // - X-Frame-Options: DENY // - Referrer-Policy: strict-origin-when-cross-origin // - Permissions-Policy: deny camera/mic/geo/USB/payment/etc. // - Cross-Origin-Opener-Policy: same-origin // - Auto-CSRF token injection + same-origin enforcement on actions // - 1 MB body-size limit on action() bodies // - Prototype-pollution guard (rejects __proto__, constructor, prototype keys)

What 'strict' adds

ts
// security: 'strict' adds: // - Cross-Origin-Embedder-Policy: require-corp (tightens to SAB-eligible) // - Stricter CSP (no 'unsafe-inline' fallbacks; no eval) // - 256 KB body-size limit on action() bodies

Content-Security-Policy

place's strict CSP is the framework's first-class output, not an afterthought. The directives that ship under 'standard':

text
// What ships when security: 'standard': Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<random>'; style-src 'self' 'nonce-<random>'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
Per-response hashes, never 'unsafe-inline'
script-src uses per-request nonces; the framework's SPA-nav runtime is the only inline script and it carries the request nonce. style-src uses per-response SHA-256 hashes for any inline style="…" attribute the SSR actually emitted — so the directive is tight without breaking author-written inline styles. Reactive style:* bindings still write through setProperty() on hydration and need no hash.

How inline styles + style:* directives stay CSP-safe

SSR renders inline style="…" attrs verbatim, but the framework collects every value it emits during the render and adds 'unsafe-hashes' 'sha256-<hash>' to the response's style-src. ISR cache hits reuse the same hash list, so the CSP is byte-stable across cache + live renders. Reactive bindings stay out of the SSR'd HTML entirely.

ts
// Inline style="…" attributes that SSR emits are CSP-safe by // per-response hash injection. The framework collects every inline // style attribute value while rendering, SHA-256 hashes them, and // adds 'unsafe-hashes' + each 'sha256-<hash>' to the response's // style-src CSP directive. Pages with no inline styles ship a // tight 'self'-only style-src; pages that do ship exactly the hashes // they need. <div style={`color: red;`}>Hi</div> // SSR emits: <div style="color: red;">Hi</div> // CSP gets: style-src 'self' 'unsafe-hashes' 'sha256-<hash>'; // Reactive style bindings (style:transform, style:opacity) write // through element.style.setProperty() at hydration — no inline // attribute, no hash needed. <div style:transform={() => `translateX(${x()}px)`} /> // → el.style.setProperty('transform', `translateX(${x()}px)`)

Auto-CSRF

State-changing routes (POST / PUT / DELETE) verify a same-origin CSRF token by default. The token issuance is zero-config — return a csrf field from load() and the framework injects a <meta name="csrf-token"> into the head; the client transport reads it back when calling actions.

ts
// Auto-CSRF: when load() returns { csrf }, the framework injects a // <meta name="csrf-token"> in the SSR'd <head>. <Form> and // action.call() auto-read it. Zero developer wiring. load: async () => ({ csrf: await issueCsrfToken(), user: await getUser(), }) // On the client, the action handler verifies the token automatically: const updateProfile = action({ path: '/profile/update', input: shape({ name: 'string' }), fn: async (input) => { /* token already validated */ }, })

Same-origin enforcement

action() handlers reject cross-origin requests unless the origin is in the allowlist. Override the default if you legitimately need cross-origin requests:

ts
// Same-origin: state-changing actions reject cross-origin requests // by default. Set the allowed origins in app() if you need to // allowlist a specific host: app({ pages: [...], security: { preset: 'standard', sameOrigin: ['https://app.example.com', 'https://staging.example.com'], }, }).run()

Body-size + prototype-pollution guards

action() enforces a body-size limit (1 MB on 'standard', 256 KB on 'strict') before any user code runs. JSON parsing rejects keys named __proto__, constructor, or prototype — the single-line patch that closes the entire class of prototype-pollution exploits.

Don't relax for ergonomics
The security defaults stay on for a reason. If a feature in your app fights with 'standard', that's signal — the framework documents the failure mode at the violation site (CSP report, body-too-large 413, CSRF reject 403). Read those, don't relax the defaults.