Best Practices
Security Headers & CSP
HTTP security headers and Content Security Policy for apps/web
Gap severity: HIGH — still open
apps/web/next.config.ts sets no security headers at all — not even X-Frame-Options or X-Content-Type-Options. (The API-side hardening — helmet — is plan phase 4.)
The cal.diy pattern
A dedicated apps/web/lib/csp.ts exports three functions:
getCspNonce()— generates a cryptographically random nonce per request.getCspPolicy(nonce)— builds the full CSP string usingnonce-${nonce}+strict-dynamicin production, andunsafe-inline unsafe-evalin development. Coversdefault-src,script-src,object-src,base-uri,child-src,style-src,font-src,img-src,connect-src.getCspHeader({ shouldEnforceCsp, nonce })— returnsContent-Security-PolicyorContent-Security-Policy-Report-Onlyheader pair.
What to implement
Static headers in next.config.ts (immediate, 30 min)
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
],
}];
},Nonce-based CSP (after Sentry is wired up)
- Create
apps/web/lib/csp.tsmodeled on cal.diy's. - Extend
apps/web/middleware.tsto generate a nonce and inject it viaNextResponse.headers:
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const { header, value } = getCspHeader({ shouldEnforceCsp: isProduction, nonce });
response.headers.set(header, value);
response.headers.set("x-nonce", nonce);- Pass the nonce into
<Script>and<style>tags via a server component that readsheaders().get("x-nonce").
Files to modify/create: apps/web/next.config.ts (add headers()), apps/web/lib/csp.ts (new), apps/web/middleware.ts (nonce injection).