| name | configure-security-headers-csp |
| description | Configures HTTP response security headers and a strict, nonce/hash-based Content-Security-Policy — script-src with a per-request nonce or sha256 hash plus 'strict-dynamic' (so you can drop host allowlists and 'unsafe-inline'), object-src 'none', base-uri 'none', frame-ancestors to control framing, a Report-Only rollout via report-to/report-uri before enforcing, plus HSTS with includeSubDomains+preload, X-Content-Type-Options: nosniff, Referrer-Policy, a deny-by-default Permissions-Policy, correct CORS (echo a single allowed origin, never wildcard '*' together with Access-Control-Allow-Credentials), and cookie flags Secure+HttpOnly+SameSite. Eliminates inline-script XSS sinks, clickjacking, MIME-sniffing, mixed content, and credentialed-CORS leaks by policy rather than per-bug patching. |
When to Use
Reach for this skill when the task is setting HTTP response headers and CSP as defense-in-depth policy, not chasing one specific vulnerability:
- "Add a Content-Security-Policy" / "our CSP uses 'unsafe-inline' — make it strict"
- "After turning on CSP the page broke: Refused to execute inline script / Refused to load the stylesheet"
- "Enable HSTS / submit the domain to the preload list"
- "Stop the site from being framed / set frame-ancestors / X-Frame-Options"
- "Set Referrer-Policy and lock down Permissions-Policy (camera, geolocation, FLoC)"
- "Our CORS sends
Access-Control-Allow-Origin: * with credentials — is that safe?" (no)
- "Cookies missing Secure/HttpOnly/SameSite" / harden the Set-Cookie flags
NOT this skill:
- Finding/fixing a concrete bug — reflected/stored XSS sink, open redirect, SSRF, SQLi — and sanitizing the offending code path → remediate-web-vulnerabilities (this skill is the header containment layer that limits the blast radius of such bugs)
- Configuring the CDN/WAF/edge that injects, caches, or overrides these headers, or rate-limits at the edge → setup-cdn-edge-waf (this skill defines the header values it should emit)
- TLS certs, cipher suites, OCSP, ACME issuance, the TLS handshake behind HSTS → configure-dns-tls (HSTS only asserts TLS is mandatory; it doesn't provision it)
- Reverse-proxy/load-balancer routing where you might also add these headers (nginx/Envoy/Traefik) → configure-reverse-proxy-lb (this skill says which headers; that one places them in the proxy)
- The OAuth/OIDC redirect, token, and session-cookie protocol → integrate-oauth-oidc / auth-jwt-session (this skill only hardens the cookie flags and CORS around them)
- Structured threat enumeration (STRIDE) or a full audit pass → threat-model-stride / security-review
- Active fuzzing/DAST to prove a bypass → fuzz-dynamic-security-test
Steps
-
Default to a strict, nonce- or hash-based CSP — host allowlists are obsolete and bypassable. Allowlist CSPs (script-src 'self' cdn.example.com) are trivially defeated via JSONP endpoints, open redirects, or AngularJS on a whitelisted host (Google's own research found ~94% of allowlist CSPs bypassable). The strict pattern:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
report-uri /csp-report; report-to csp
'strict-dynamic' lets a nonced/hashed script load further scripts it creates, so you don't enumerate every CDN. When present, browsers that understand it ignore https: and 'unsafe-inline' — those are fallbacks for old browsers only, not a real relaxation.
object-src 'none' kills Flash/<object> injection; base-uri 'none' stops <base href> from rewriting relative script URLs.
- You usually don't need
default-src micromanaged once script-src is strict; the dangerous directive is script execution.
-
Generate a fresh 128-bit nonce per response and stamp it on every inline <script>. The nonce must be cryptographically random and unique per HTTP response (never reuse, never hardcode) — a static nonce is equivalent to 'unsafe-inline'.
| Stack | Generate | Apply |
|---|
| Express | res.locals.nonce = crypto.randomBytes(16).toString('base64') | helmet contentSecurityPolicy with (req,res)=>nonce;
|
| Next.js | nonce in middleware.ts, pass via header | Next injects nonce into its own scripts when CSP header has a nonce |
| Django | django-csp @csp_update / {{ request.csp_nonce }} | <script nonce="{{ request.csp_nonce }}"> |
| Rails |
Common Errors
- Allowlist CSP with
'unsafe-inline'. script-src 'self' 'unsafe-inline' provides essentially zero XSS protection — inline injected scripts run. Fix: nonce/hash + 'strict-dynamic', drop 'unsafe-inline' (keep it only as the old-browser fallback that strict-dynamic neutralizes).
- Reusing or hardcoding the nonce. A static/cached nonce =
'unsafe-inline'; the attacker just reads it from the page and reuses it. Fix: fresh CSPRNG nonce per response; for cacheable HTML use hashes instead.
- Flipping enforcing CSP straight to prod. You blank-screen real users on day one. Fix:
-Report-Only first, collect via report-to/report-uri, fix breakage, then enforce.
'unsafe-eval' left in to satisfy a library. Re-opens eval/Function injection. Fix: move to a CSP-compatible build (no runtime eval); add Trusted Types (require-trusted-types-for 'script') instead of loosening.
- CSP only on HTML, missing
object-src/base-uri. <base> hijack or <object> injection bypasses a script-only policy. Fix: always add object-src 'none'; base-uri 'none'.
Access-Control-Allow-Origin: * (or reflected Origin) with Allow-Credentials: true. Any website reads the victim's authenticated data. Fix: allowlist + echo the single matched origin + Vary: Origin; or drop credentials and use *.
- Substring origin matching.
origin.endsWith('example.com') allows notexample.com/example.com.evil.com. Fix: exact full-origin set membership.
- HSTS
preload added prematurely / without includeSubDomains. A non-HTTPS subdomain becomes unreachable, and preload removal takes months. Fix: confirm every subdomain is HTTPS-only before includeSubDomains; preload; ramp max-age up gradually.
- Setting HSTS over plain HTTP. Ignored by browsers and a sign of misconfig. Fix: emit HSTS only on HTTPS responses; redirect HTTP→HTTPS first.
- Cookies without //. XSS steals the session; CSRF rides it; it leaks over HTTP. Fix: .
Verify
- Scan the live headers: run the response through
securityheaders.com / Mozilla Observatory, or curl -sI https://site — confirm a single Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and frame-ancestors present, with no duplicates.
- CSP is strict: the policy contains a
'nonce-...' or 'sha256-...' in script-src with 'strict-dynamic' and no standalone 'unsafe-inline'/'unsafe-eval' that a modern browser honors; object-src 'none' and base-uri 'none' present. Validate with Google's CSP Evaluator.
- Nonce is per-response: fetch the page twice — the nonce value differs each time and matches the inline
<script nonce=...> tags.
- Report-Only worked: the violation collector received reports and they were triaged before enforcing; the enforced policy doesn't blank the app (load the real pages, check the console for
Refused to...).
- CORS is safe:
curl -H 'Origin: https://evil.com' -I to a credentialed endpoint returns no Access-Control-Allow-Origin for evil.com (or omits credentials); an allowlisted origin gets that exact origin echoed plus Vary: Origin. No *+credentials anywhere.
- Cookies hardened:
Set-Cookie on the session cookie shows Secure; HttpOnly; SameSite=... (and __Host- prefix for session); inspect in DevTools → Application → Cookies.
- HSTS sane:
Strict-Transport-Security only on HTTPS, max-age ≥ 1 year, includeSubDomains only if every subdomain is HTTPS; preload only when committed (verify at hstspreload.org).
- Clickjacking blocked: attempt to frame the site from another origin → blocked by
frame-ancestors; X-Content-Type-Options: nosniff confirmed so a text/plain API body isn't sniffed to HTML.
Done = a strict nonce/hash CSP with 'strict-dynamic' and no honored 'unsafe-inline', rolled out via Report-Only then enforced; HSTS (preload only when safe), nosniff, frame-ancestors, Referrer-Policy and a deny-by-default Permissions-Policy all present exactly once; CORS validates origin against an allowlist and never pairs */reflected-origin with credentials; and session cookies carry Secure+HttpOnly+SameSite (__Host- prefixed) — all proven by the header scan, CSP evaluator, and CORS/cookie checks above.