KwadMarket Docs
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 using nonce-${nonce} + strict-dynamic in production, and unsafe-inline unsafe-eval in development. Covers default-src, script-src, object-src, base-uri, child-src, style-src, font-src, img-src, connect-src.
  • getCspHeader({ shouldEnforceCsp, nonce }) — returns Content-Security-Policy or Content-Security-Policy-Report-Only header pair.

What to implement

Static headers in next.config.ts (immediate, 30 min)

apps/web/next.config.ts
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)

  1. Create apps/web/lib/csp.ts modeled on cal.diy's.
  2. Extend apps/web/middleware.ts to generate a nonce and inject it via NextResponse.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);
  1. Pass the nonce into <Script> and <style> tags via a server component that reads headers().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).

On this page