RedScore.ai

Fixes

Web Application Security · Updated 2026-05-02

Content-Security-Policy (CSP)

Restricts which scripts and resources the page can load. Pass needs default-src or script-src. Use Report-Only first, then enforce.

Content Security Policy (CSP, W3C standard) tells browsers which sources of scripts, styles, images, frames, and other resources the page may load. A well-formed CSP is the strongest single defense against XSS: even if an attacker injects a script tag into your page, the browser refuses to execute it because the source does not match your policy. CSP is also the hardest security header to roll out because it requires inventorying every legitimate third-party resource your site uses.

RedScore awards full credit for an enforced CSP with default-src or script-src, partial credit for a Report-Only CSP with the same directives (rewarding rollout posture even before enforcement), and zero for no CSP or CSP containing only upgrade-insecure-requests.

How the check works

Per primary host, the check looks at two response headers: Content-Security-Policy (enforced) and Content-Security-Policy-Report-Only. A policy is considered meaningful if it contains either default-src or script-src.

  • Enforced CSP with default-src or script-src: 15/15 pts.
  • Report-Only CSP with default-src or script-src: 10/15 pts (2/3 credit, rewards mature rollout posture).
  • No CSP, or CSP containing only upgrade-insecure-requests: 0/15 pts.

What this check does NOT validate today: directive specificity (unsafe-inline, unsafe-eval, * wildcards all pass), nonce or hash usage, or completeness of the policy. A CSP of "default-src *" passes the same as a tightly-scoped policy. The recommended values below are best practice, not what the score requires.

How the verdict maps to evidence

  • Pass (15/15 per host): enforced Content-Security-Policy header with default-src or script-src.
  • Partial pass (10/15 per host): Content-Security-Policy-Report-Only with default-src or script-src.
  • Fail (0/15 per host): no CSP, or only upgrade-insecure-requests.

Fix: a phased CSP rollout

Do not enforce a strict CSP on day one. default-src 'self' breaks every external script, every inline event handler, and every stylesheet not on your own origin. The standard rollout: publish in Report-Only mode, collect violation reports for at least two weeks, fix or unblock legitimate sources, then move to enforce.

Step 1: minimal Report-Only policy

Start with a policy that allows everything you currently load and reports violations. The goal is to capture every legitimate source before tightening anything.

Report-Only starter policy

Content-Security-Policy-Report-Only: default-src 'self' 'unsafe-inline' 'unsafe-eval' https: data:; report-uri https://csp.yourdomain.tld/report; report-to csp-endpoint

This policy allows almost anything, so nothing breaks. The browser still sends violation reports to your endpoint for things that fall outside even this generous policy. After deploying, watch the report endpoint for a week to see what actually loads.

Step 2: tighten in Report-Only

Once you know your sources, tighten the policy. Replace https: with explicit hostnames; remove unsafe-inline and unsafe-eval where you can. Continue running Report-Only and watch reports for two more weeks.

Tighter Report-Only policy

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com https://cdn.example-analytics.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https:;
  connect-src 'self' https://api.example-analytics.com;
  frame-ancestors 'none';
  report-uri https://csp.yourdomain.tld/report;

Step 3: enforce

When violation reports settle and only show actual attacks (or known-okay edge cases), switch the header name from Content-Security-Policy-Report-Only to Content-Security-Policy. The policy is now enforced; violations are blocked, not just reported.

Step 4: progressively remove unsafe-inline

If you still need 'unsafe-inline' for scripts or styles, the path forward is nonces or hashes:

  • Nonces: generate a random nonce per request, add it to every legitimate inline script tag (nonce="...") and to your CSP as 'nonce-...'. Browsers only execute inline scripts whose nonce matches.
  • Hashes: compute SHA-256 of each inline script's exact content, add 'sha256-...' to your CSP. Static inline scripts work; dynamic ones do not.
  • strict-dynamic: pair with nonces or hashes; lets nonced scripts load further scripts dynamically without listing every URL.

Per-server snippets

nginx (Report-Only)

add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;

Apache (enforced)

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-RANDOM'"

Caddy

header Content-Security-Policy "default-src 'self'; script-src 'self'"

Cloudflare (Transform Rules)

Set response header via Transform Rules:
  Header name:  Content-Security-Policy
  Header value: default-src 'self'; script-src 'self' https://trusted.cdn.com

Express / Node.js (Helmet)

import helmet from "helmet";

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "https://trusted.cdn.com"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
  },
}));

Reporting endpoints

report-uri is the legacy directive (still supported). report-to is the modern one and uses the Reporting API. Use both for compatibility:

  • Self-hosted: a small endpoint that accepts POST requests with application/csp-report content-type and stores them.
  • Managed: report-uri.com, Datadog CSP reporter, Sentry CSP reporting, or your APM vendor's CSP integration.

Verify the fix

  • curl -sI https://yourdomain.tld | grep -iE 'content-security-policy' should show your header.
  • csp-evaluator.withgoogle.com checks your policy for known weaknesses (overly broad sources, missing directives, unsafe-inline without nonces, etc.).
  • Open the page in a browser. Open DevTools console. CSP violations log explicitly with the blocked URL and which directive blocked it.
  • Re-run the RedScore lookup. Pass requires the header on every primary HTTPS host with at least default-src or script-src.

Common pitfalls

  • Enforcing too early. Skipping Report-Only and going straight to enforce breaks pages that depend on third-party scripts you forgot to list. Always run Report-Only for at least two weeks.
  • Using 'self' but loading bundled scripts from a CDN. 'self' only matches your origin. CDN-hosted assets need their hostnames listed explicitly (or 'self' replaced with strict-dynamic and nonces).
  • Forgetting style-src for stylesheets. CSS is governed by style-src; if default-src is 'self' but your stylesheets come from Google Fonts, fonts.googleapis.com must be in style-src.
  • Inline event handlers (onclick attributes, <a onclick="...">). These are inline scripts. Either move them to external listeners (recommended), allow 'unsafe-inline' (defeats much of CSP), or use 'unsafe-hashes' with the exact hash.
  • Mixing Report-Only and enforced headers. Both can coexist; the enforced one blocks while Report-Only reports against a more aggressive future policy. Useful as a staging mechanism, but keep the relationship explicit so you do not get confused about which one is live.
  • report-to without the Reporting-Endpoints response header. Modern report-to requires a paired Reporting-Endpoints header naming the endpoint. Include both, or fall back to report-uri.
  • CSP overridden at one layer. CDN, load balancer, and origin can all set CSP. Multiple CSP headers are valid; they intersect, becoming the strictest combination, which is usually not what you want. Set CSP at one layer and confirm with curl.

What to do next

See how these recommendations apply to your site's current scan results.

Scan domain