DEV Community

Cover image for Understanding Content Security Policy (CSP)
Ozan
Ozan

Posted on

Understanding Content Security Policy (CSP)

TL;DR

CSP is an HTTP header that tells browsers "only run scripts from these trusted sources." It's your defense against XSS attacks where hackers inject malicious JavaScript into your site. Start with Content-Security-Policy-Report-Only to see what breaks, fix inline scripts by adding nonce attributes, whitelist external domains you trust, then enforce with Content-Security-Policy.

Test using Google's CSP Evaluator. Avoid 'unsafe-inline' and 'unsafe-eval' like the plague—they defeat the whole purpose.


Image Source: https://www.writesoftwarewell.com/content-security-policy/

Introduction

CSP (Content Security Policy) is basically a bouncer for your website. It tells the browser, "Hey, only let in scripts, images or fonts from these approved sources."

Why should you care? Because XSS (Cross-Site Scripting) attacks are everywhere, and CSP is one of the best ways to protect your users from them.

What is Content Security Policy?

Think of your website as a nightclub. Without CSP, anyone can walk in and start doing whatever they want. With CSP, you're the bouncer with a guest list—you explicitly tell the browser which scripts, styles, and other resources are allowed to run.

Here's the basic idea: instead of the browser trusting everything by default, you flip the script. You say "only trust content from these specific places, and block everything else."

A Simple Example

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com 
Enter fullscreen mode Exit fullscreen mode
  • default-src 'self' - "By default, only load stuff from my own domain"
  • script-src 'self' https://trusted-cdn.com - "For JavaScript specifically, allow my domain and this one CDN I trust"

If someone tries to inject a <script> tag pointing to evil-hacker-site.com, the browser just... won't run it. Pretty neat, righ

It's Not Just About XSS

CSP also protects against:

  • Clickjacking - Someone embedding your site in an iframe to trick users
  • Mixed content attacks - Loading HTTP resources on HTTPS pages
  • Malicious iframes - Unexpected content being loaded into your page
  • Unauthorized data exfiltration - Scripts trying to send data to random domains

Think of it as security insurance. Your input validation might have a bug. Your sanitization library might miss something. CSP is there as a backup.

The Nonce Trick: When You Actually Need Inline Scripts

Okay, so you've got some inline JavaScript that you really need to keep inline. Maybe it's server-rendered data, or a legacy widget that's hard to refactor. What do you do?

Enter the nonce attribute. It's basically a one-time password for your inline scripts.

How Nonces Work

  1. For every page request, your server generates a random, unguessable string:
 const nonce = crypto.randomBytes(16).toString('base64'); // Result: something like "dGhpcyBpcyBhIG5v==" 
Enter fullscreen mode Exit fullscreen mode
  1. Add this nonce to your CSP header:
 Content-Security-Policy: script-src 'self' 'nonce-dGhpcyBpcyBhIG5v==' 
Enter fullscreen mode Exit fullscreen mode
  1. Add the same nonce to your inline script:
 <script nonce="dGhpcyBpcyBhIG5v=="> console.log('This inline script is allowed!'); </script> 
Enter fullscreen mode Exit fullscreen mode
  1. The browser checks: "Does the nonce in the script match the nonce in the CSP header? Yes? Cool, run it."

Why this works: An attacker can't inject a script with the correct nonce because they don't know what the random value is. It changes with every page load.

Practical Example: Server-Side Rendering

Let's say you're using Node.js with Express and EJS:

app.get('/dashboard', (req, res) => { // Generate a random nonce const nonce = crypto.randomBytes(16).toString('base64'); // Set CSP header with the nonce res.setHeader( 'Content-Security-Policy', `script-src 'self' 'nonce-${nonce}'` ); // Pass the nonce to your template res.render('dashboard', { nonce, userData: {...} }); }); 
Enter fullscreen mode Exit fullscreen mode

In your EJS template:

<script nonce="<%= nonce %>"> const userData = <%= JSON.stringify(userData) %>; console.log('User data loaded:', userData); </script> 
Enter fullscreen mode Exit fullscreen mode

The nonce changes on every request, so even if an attacker sees it once, it's useless for their attack.

Important: What Nonces DON'T Protect Against

Nonces are great, but they won't save you from:

  • XSS vulnerabilities in your inline code itself
  • Scripts loaded from whitelisted domains that are compromised
  • JavaScript that's already on the page and gets called with malicious input

They're a way to say "this inline script is mine," not "this inline script is safe."

Key CSP Directives

Here are the most important directives you should know:

  • default-src: Fallback for other directives
  • script-src: Controls where scripts can be loaded from
  • style-src: Controls stylesheets
  • img-src: Controls images
  • connect-src: Controls AJAX, WebSocket, and fetch requests
  • font-src: Controls fonts
  • frame-src: Controls frames and iframes
  • frame-ancestors: Controls where your page can be embedded
  • base-uri: Controls the <base> tag
  • form-action: Controls form submission targets

Common CSP Values

  • 'self': Same origin as the document
  • 'none': Block everything
  • 'unsafe-inline': Allow inline scripts/styles (avoid if possible!)
  • 'unsafe-eval': Allow eval() and similar (avoid if possible!)
  • https:: Allow any HTTPS resource
  • https://example.com: Allow specific domain
  • 'nonce-{random}': Allow specific inline script with matching nonce
  • 'sha256-{hash}': Allow specific inline script with matching hash

How to Actually Implement CSP (Without Breaking Everything)

Here's the thing: you can't just slap a strict CSP on your site and call it a day. If you do, everything will break and your users will revolt. Trust me, many developers have learned this the hard way.

Here's the smart approach:

Step 1: Start in Report-Only Mode

Use the Content-Security-Policy-Report-Only header first:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations 
Enter fullscreen mode Exit fullscreen mode

This is genius because the browser will:

  • Not block anything - Your site works normally
  • Report violations - You get a JSON report of what would have been blocked

You're basically doing a dry run.

Step 2: Set Up a Violation Report Endpoint

Create an endpoint to catch these reports:

// Example Express.js endpoint app.post('/csp-violations', express.json({ type: 'application/csp-report' }), (req, res) => { console.log('CSP would have blocked:', req.body); // Maybe save to database for analysis res.status(204).end(); }); 
Enter fullscreen mode Exit fullscreen mode

Or just use a service like report-uri.com that visualizes everything for you.

Step 3: Fix Your Code Based on Reports

You'll probably discover:

  • Inline scripts everywhere (especially in legacy code)
  • Scripts loaded from CDNs you forgot about
  • Third-party widgets that load their own stuff
  • Inline event handlers like onclick="doThing()"

Time to refactor! Move inline scripts to external files, or use nonce attributes (more on that in a sec).

Step 4: Switch to Enforcement Mode

Once the reports stop coming (or you've whitelisted legitimate sources), change from Content-Security-Policy-Report-Only to Content-Security-Policy.

Now you're actually protecting your users!

Step 5: Keep Both Headers Running

Pro tip: Keep report-only mode running even after you enforce. Use it to test stricter policies:

Content-Security-Policy: default-src 'self' Content-Security-Policy-Report-Only: default-src 'none'; script-src 'self' 
Enter fullscreen mode Exit fullscreen mode

This way you can see what a stricter policy would break before you deploy it.

Testing Your CSP

Using Google's CSP Evaluator

Google provides an excellent tool for testing your CSP: CSP Evaluator

Simply paste your CSP header, and it will:

  • Highlight security issues
  • Suggest improvements
  • Explain potential vulnerabilities

Manual Testing

  1. Check browser console: CSP violations appear in the console
  2. Test inline scripts: Try adding an inline script—it should be blocked
  3. Test external resources: Try loading resources from non-whitelisted domains
  4. Use browser DevTools: Network tab shows blocked resources

Example Test

<!-- This should be blocked with a proper CSP --> <script>alert('This is an inline script')</script> <!-- This should also be blocked --> <img src="https://untrusted-domain.com/image.jpg"> 
Enter fullscreen mode Exit fullscreen mode

Common CSP Mistakes (And How to Fix Them)

1. Using 'unsafe-inline' (The Cardinal Sin)

Content-Security-Policy: script-src 'self' 'unsafe-inline' 
Enter fullscreen mode Exit fullscreen mode

Why this is terrible: This is like installing a security door and then leaving it wide open. It allows any inline script to run, including the ones attackers inject. You just defeated the entire purpose of CSP.

The fix: Use nonce attributes instead (see below) or move your scripts to external files.

2. Using 'unsafe-eval' (Also Bad)

Content-Security-Policy: script-src 'self' 'unsafe-eval' 
Enter fullscreen mode Exit fullscreen mode

Why this sucks: This allows eval(), new Function(), and similar code execution methods. Attackers love this.

The fix: Refactor your code. Modern JavaScript rarely needs eval(). If you think you need it, you probably don't.

3. Whitelisting Everything with Wildcards

Content-Security-Policy: script-src * 
Enter fullscreen mode Exit fullscreen mode

What you just did: "Allow scripts from literally anywhere on the internet"

The fix: Be specific. List the exact domains you trust.

4. Forgetting About base-uri

If you don't set base-uri, attackers can inject <base> tags to hijack relative URLs.

The fix: Always include base-uri 'self' or base-uri 'none' in your policy.

5. Whitelisting Entire CDNs

Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com 
Enter fullscreen mode Exit fullscreen mode

The problem: CDNs host thousands of libraries. An attacker can use any of them, including old vulnerable versions of jQuery.

The fix: Use Subresource Integrity (SRI) hashes:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK" crossorigin="anonymous"></script> 
Enter fullscreen mode Exit fullscreen mode

Now the browser will only run it if the hash matches.

6. Trusting JSONP Endpoints

If you whitelist a domain that has JSONP endpoints, attackers can abuse them to bypass your CSP.

The fix: Don't whitelist domains with JSONP. Use CORS-enabled APIs instead.

Real-World Example: Strict CSP

Here's an example of a strict, production-ready CSP:

Content-Security-Policy: default-src 'none'; script-src 'self' 'nonce-{random}'; style-src 'self' 'nonce-{random}'; img-src 'self' https://trusted-images.com; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests; 
Enter fullscreen mode Exit fullscreen mode

This policy:

  • Blocks everything by default
  • Only allows scripts and styles from same origin with nonces
  • Restricts images to same origin and a trusted CDN
  • Prevents the page from being framed
  • Forces HTTPS upgrades

Best Practices

  1. Start strict, then relax if needed: It's easier to add exceptions than to tighten a loose policy
  2. Use nonces for inline scripts: Generate a new random nonce for each page load
  3. Avoid 'unsafe-inline' and 'unsafe-eval': These significantly weaken your CSP
  4. Monitor violations: Keep report-only mode running alongside enforcement
  5. Use SRI for third-party resources: Add integrity checks to external scripts and styles
  6. Keep your policy maintainable: Document why each source is whitelisted
  7. Test thoroughly: Use automated tests to ensure CSP doesn't break functionality
  8. Update regularly: As your app evolves, keep your CSP up to date

Beyond XSS: Other Security Benefits

Preventing Clickjacking

Content-Security-Policy: frame-ancestors 'none' 
Enter fullscreen mode Exit fullscreen mode

This prevents your site from being embedded in iframes, protecting against clickjacking attacks.

Enforcing HTTPS

Content-Security-Policy: upgrade-insecure-requests 
Enter fullscreen mode Exit fullscreen mode

This automatically upgrades HTTP requests to HTTPS.

Restricting Form Targets

Content-Security-Policy: form-action 'self' 
Enter fullscreen mode Exit fullscreen mode

This prevents forms from submitting to external domains, which could be used for phishing.

Debugging CSP Issues

When things don't work:

  1. Check the console: CSP violations are logged with detailed information
  2. Use Report-Only mode: Test changes without breaking production
  3. Verify nonce generation: Ensure nonces are unique per request
  4. Check for typos: CSP syntax is strict
  5. Test in multiple browsers: Implementation may vary slightly

Wrapping Up

Look, CSP isn't a magic bullet. You still need to validate inputs, escape outputs, and follow other security best practices. But it's one of the most powerful tools in your security toolkit.

Here's what you need to remember:

The Good News:

  • CSP blocks most XSS attacks, even if your other defenses fail
  • Modern browsers all support it
  • It's not that hard to implement if you do it gradually

The Reality Check:

  • You can't just flip a switch—you need to refactor your code
  • Legacy codebases with inline scripts everywhere will be a pain
  • You'll probably break something in production at least once (we all do)

Your Action Plan:

  1. Start with report-only mode TODAY (seriously, it's free intel)
  2. Fix the low-hanging fruit (inline event handlers, obvious inline scripts)
  3. Use nonces for the stuff you can't easily refactor
  4. Test with Google's CSP Evaluator
  5. Gradually tighten your policy over time

When You Really Need CSP:

  • You handle sensitive data (payments, healthcare, personal info)
  • You have user-generated content (comments, forums, profiles)
  • You want to sleep better at night

When You Can Probably Skip It:

  • Static blog with no user interaction
  • Internal tools behind authentication with no user input
  • You're just starting out and have bigger fish to fry

Final thought: Start strict and relax if needed. It's way easier than starting loose and trying to tighten things later.

Now go secure your websites! 🔒

Further Reading and Resources


Stay secure, and happy coding!

Top comments (0)