Browser security boundaries

Add a Content Security Policy

Use CSP to restrict script and resource execution while keeping output handling and application design as the primary XSS defenses.

A Content Security Policy tells the browser what a page may load and run. Scripts, styles, images, frames, connections: each gets its own allowlist. It’s a strong second layer. Treat it as a safety net behind correct output escaping, not as permission to render unsafe HTML.

Start in report-only mode

The mistake I see most often is turning on enforcement on day one and breaking the site. Do the opposite. Start with the report-only header. Violations are reported, nothing is blocked, and you learn what the page really needs:

Content-Security-Policy-Report-Only:
  default-src 'self'; script-src 'self'; report-uri /csp-report

Point report-uri at a small endpoint that logs the body. Each report names the blocked resource and the directive that would have stopped it:

{
  "csp-report": {
    "document-uri": "https://app.flaviocopes.com/checkout",
    "violated-directive": "script-src",
    "blocked-uri": "https://cdn.stripe.com/v3.js"
  }
}

Now you build the real policy from evidence, not from guessing. That report says checkout needs https://cdn.stripe.com in script-src. Add it, redeploy, watch the reports again.

Use nonces, not exceptions

Once the reports go quiet, switch to enforcement. Inline scripts are the tricky part. The lazy fix is 'unsafe-inline', which throws away most of the protection. The right fix is a per-response nonce, a random value the server generates for every page load:

const nonce = crypto.randomBytes(16).toString('base64')
res.set('Content-Security-Policy',
  `default-src 'self'; script-src 'self' 'nonce-${nonce}'`)

Then put the same value on each inline script you wrote:

<script nonce="RANDOM_PER_RESPONSE">/* only this inline script runs */</script>

An injected <script> doesn’t know the nonce, so the browser refuses it and logs Refused to execute inline script because it violates the following Content Security Policy directive in the console. Your own scripts keep working.

The exception that eats the policy

A team adds an analytics snippet. It needs inline code, so someone adds 'unsafe-inline' to script-src. The site works again. But that one exception also lets an attacker’s injected inline script run on every page. The policy is still there, and it protects nothing.

When you hit this, don’t add the exception. Give the snippet a nonce, move it to a file under 'self', or use a hash of its exact content. The report-only header will tell you when you got it right.

Try this on your own project: deploy the report-only policy and click through the home page, sign-in, and checkout. Write down the violations that are real requirements, remove one source you don’t need, then turn on enforcement and inject an inline alert(1) through a form field. It should not run.

Lesson completed