Enforce a CSP, COOP and Trusted Types

Featurekamo-marketing
Shipped
August 20, 2026 at 3:59 AM UTC
Author
Kamo
Commit
b34d847

Adds the three headers the Trust and Safety audit asked for, plus Referrer-Policy and Permissions-Policy. Every origin in the policy was taken from what the site actually loads rather than guessed — the chat widget bundle on theme.kamocrm.com (which opens a WebSocket to media. and calls api. and capcha.), the analytics script on analytics., the hero's HLS ladder, and the ***'s challenge endpoint. Audited the built page against the finished policy resource by resource: zero blocked. ON script-src 'unsafe-inline' — this is a real compromise and worth stating rather than burying. The strong form of a CSP is a per-response nonce with 'strict-dynamic', and it is not available here: a nonce means reading headers() during render, which opts the route out of static generation. This app has ~200 prerendered routes and their being static is load-bearing — a dynamic route with no loading boundary prefetches nothing, so every link click starts cold. There is no hash-based escape either, because the inline scripts are Next's own RSC flight data, whose contents differ per page and per build. So: this CSP does NOT stop an injected inline script. What it does do is confine every external script, fetch, frame, image and media load to known origins, forbid plugins, pin the base URI so injected markup cannot re-root relative URLs, and stop form posts to third parties. TRUSTED TYPES took three attempts to make safe, and the failures are the interesting part: 1. A plain inline <script> first in the layout's <head> is not first. React hoists Next's `<script async src>` tags above anything the layout renders, so the policy landed after twelve of them — and async scripts execute the moment they finish downloading. Turbopack's runtime is among them and does `createElement("script")` then sets `.src`, the exact sink that throws without a policy. 2. `next/script` with strategy="beforeInteractive" did not move it either; still twelve scripts ahead. 3. What makes it safe is that no sink can fire that early. All 78 flight-data pushes are in the body — first at offset 193,072, with </head> at 11,940 — so React cannot begin hydrating until well after the policy is installed, and there is no sink-touching code in <head> before it. Verified on the built HTML rather than assumed. Two things would have broken without being grepped for. The *** widget is a Svelte build and creates a policy named `svelte-trusted-html`; a policy name is a runtime string, so nothing about it surfaces in types, lint or the build, and omitting it from the allow-list would have thrown and taken the contact and demo forms down. And hls.js and the *** both build Web Workers from createObjectURL, so the policy has to pass `blob:` before its origin test — a blob: URL has no useful .origin and would otherwise have failed it, taking the hero reel with it. The policy's createScriptURL is checked against kamocrm.com, which is strictly more permissive than script-src, so it can never refuse something the CSP allows. createHTML passes through and does NOT sanitise: a real sanitiser would have to ship on the critical path and could silently mangle the JSON-LD blocks and the widget's markup. That half is a chokepoint and an audit hook, not a filter — and it is where to harden next. Cross-Origin-Resource-Policy was written and then removed. Applied across `/:path*` it covers the Open Graph images, which exist specifically to be embedded from other origins. frame-ancestors is 'self' to match the X-Frame-Options: SAMEORIGIN already being served, rather than tightening to 'none' and breaking an embed nobody remembered. Verified against the real standalone server, not just the config: headers present on both a prerendered route and a *** route, and the long-lived asset Cache-Control still intact.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing