Skip to main content
All fixes

SSL, TLS, security headers

Deploy a Content-Security-Policy to mitigate XSS attacks

No CSP header means any injected script runs with full page privileges. Add a strict policy with nonce-based script-src, frame-ancestors, and upgrade-insecure-requests.

What's happening

Content-Security-Policy (CSP) is an HTTP response header that constrains which sources the browser will load resources from and which inline content it will execute. Without CSP, an attacker who manages to inject a tag — via stored XSS, reflected XSS, or DOM-based XSS — can execute arbitrary JavaScript with the same privileges as the page itself.

A correctly configured CSP turns most XSS bugs into defense-in-depth opportunities rather than full account takeover. Strict CSP using nonces or hashes for inline scripts, plus 'strict-dynamic' to allow third-party scripts loaded by trusted bootstrap code, is the modern recommendation from Google's CSP team.

The fix is to deploy CSP in Report-Only mode first, fix the violations it surfaces, then promote to enforce mode. Skipping the Report-Only stage almost always breaks production analytics, payment widgets, or admin dashboards.

Why it matters

Without CSP, every XSS bug — even one in a low-traffic admin page — can be escalated to full session takeover, credential theft, or supply-chain compromise. CSP turns a high-severity bug into a medium-severity one.

OWASP ASVS Level 2 requires CSP on all production applications. PCI-DSS 4.0 lists CSP under script integrity requirements for payment pages. Bug-bounty programs accept missing-CSP findings as a separate issue from the underlying XSS.

Mozilla Observatory, SecurityHeaders.com, and most internal security scorecards penalize a missing CSP heavily — typically a full letter grade.

Common causes

  • The application has never deployed CSP and the team has not allocated time to identify all script sources.
  • An old CSP was removed because it broke production and was never re-introduced safely.
  • Inline scripts and styles are everywhere, making strict CSP non-trivial.
  • Third-party widgets (analytics, ads, chat, A/B testing) load arbitrary scripts and the team has not enumerated them.
  • Single-page application uses eval() or new Function() somewhere in the bundler output.

Detect this on your site

Run a quick scan with the Full Site Audit. The tool surfaces this exact issue with the records and context needed to apply the fix below.

Open Full Site Audit

How to fix it

  1. 1

    Inventory script sources

    Open the page in Chrome DevTools, switch to the Network tab, filter by JS, and list every distinct host. Add CSS, font, image, and connect (XHR/fetch) origins. This gives you the source allowlist for the directives.

  2. 2

    Deploy a Report-Only header first

    Set Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-RANDOM'; report-uri /csp-report and roll it to production. Collect violations for at least one full week to catch all user paths. Use a service like report-uri.com or roll your own collector.

  3. 3

    Tighten with nonces, not allowlists

    Modern strict CSP is script-src 'nonce-{random}' 'strict-dynamic';. Generate a fresh nonce per request and set it on every legitimate inline script via the nonce attribute. 'strict-dynamic' allows scripts loaded by trusted scripts, eliminating the need for long allowlists.

  4. 4

    Add structural directives

    Set frame-ancestors 'none' (or 'self') to replace X-Frame-Options. Set base-uri 'self' to prevent base-tag injection. Set form-action 'self' to prevent form-action hijacking. Set upgrade-insecure-requests to clean up residual mixed content.

  5. 5

    Promote from Report-Only to enforce

    After the violation rate has dropped to zero or near-zero, change the header from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the report-uri so you catch regressions.

  6. 6

    Set the header in your web server

    In nginx: add_header Content-Security-Policy "..." always;. In Apache: Header always set Content-Security-Policy "...". In Next.js, set it in middleware or next.config.js headers(). Generate the nonce per request in the rendering layer.

  7. 7

    Re-audit and lock in

    Re-run the Full Site Audit at /check and verify the CSP is parsed and enforced. Mozilla Observatory should grade A+. Add a CI test that ensures the header exists on every response.

Example

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0mB4se64=' 'strict-dynamic'; style-src 'self' 'nonce-r4nd0mB4se64='; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests; report-uri https://example.com/csp-report

Strict nonce-based CSP suitable for production

Frequently asked

Yes — every modern browser since 2016 enforces nonce-based CSP. The nonce attribute on the script tag must match the nonce in the header character for character. The nonce should be at least 128 bits of entropy and rotated per request.

You can but it neutralizes most of the XSS protection. Modern frameworks (Next.js Remix Rails 7) support nonce injection in templates and CSPs. Migrate the remaining inline content over a few sprints rather than living with 'unsafe-inline' permanently.

Related fixes