Content Security Policy for JavaScript Apps
Learn how to implement Content Security Policy in your JavaScript applications to prevent XSS attacks and secure your code. Practical examples with nonces, hashes, and framework-specific patterns.
While I was looking over some production security incidents the other day, I noticed a pattern that made me cringe. Nearly every XSS vulnerability I investigated could have been prevented with a proper Content Security Policy. I was once guilty of treating CSP as an afterthought—something to "add later" when the app was done. Little did I know that retrofitting CSP into an existing application is ten times harder than building with it from the start.
Why Content Security Policy Matters in Modern JavaScript Apps
Here's the uncomfortable truth: your JavaScript application is running in an environment you don't fully control. The browser is executing code, loading resources, and making network requests based on what it finds in your HTML. When I finally decided to take security seriously, I realized that CSP is essentially a whitelist that tells the browser: "Only execute code and load resources from these approved sources."
Without CSP, a single XSS vulnerability can compromise your entire application. An attacker injects malicious JavaScript, and the browser happily executes it because it has no way of knowing it wasn't supposed to be there. I cannot stress this enough! CSP is your last line of defense when input validation and output encoding fail.
The beautiful thing about CSP is that it works even if you make mistakes elsewhere. It's a security net that catches attacks at the browser level, preventing malicious scripts from executing even if they somehow make it into your DOM.
Understanding CSP Directives: The Building Blocks
When I first looked at a CSP header, it seemed like cryptic nonsense. But the directives are actually quite logical once you understand what they're protecting.
The most critical directive is script-src, which controls where JavaScript can be loaded from. In other words, this directive determines which scripts the browser will execute. Next up is style-src for CSS, img-src for images, and connect-src for AJAX requests and WebSocket connections.
Here's what a basic CSP header looks like in practice:
// Express.js middleware example
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.yourdomain.com; " +
"font-src 'self' https://fonts.gstatic.com;"
);
next();
});The default-src 'self' directive is your fallback—it tells the browser to only load resources from your own domain unless you specify otherwise. I always start with this restrictive baseline and then open up specific directives as needed.

Implementing CSP Headers in Your JavaScript Application
Luckily we can implement CSP headers in multiple ways depending on your stack. The most robust approach is setting HTTP headers at the server level. I came across many developers who tried using meta tags instead, but meta tags can't use report-only mode and are easier to manipulate.
For a Node.js/Express application, I recommend using the helmet middleware:
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-{RANDOM}'"],
styleSrc: ["'self'", "'nonce-{RANDOM}'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.yourdomain.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
})
);Notice the 'nonce-{RANDOM}' placeholder? That's where nonces come in, which brings us to the next critical topic.
Handling Inline Scripts: Nonces vs Hashes vs unsafe-inline
This is where I made my biggest mistake early on. I saw 'unsafe-inline' and thought, "Perfect! This will let my inline scripts work." Wrong. That directive completely defeats the purpose of CSP because it allows any inline script to execute—including attacker-injected ones.
There are three ways to handle inline scripts, and I've used all of them in production:
Nonces are random tokens generated on each request. You include the nonce in your CSP header and in the script tag. The browser only executes scripts with matching nonces:
// Server-side (Express + EJS example)
app.get('/dashboard', (req, res) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'`
);
res.render('dashboard', { nonce });
});
// In your HTML template
// <script nonce="<%= nonce %>">
// console.log('This script will execute');
// </script>Hashes work by calculating a SHA-256 hash of your inline script content. This is wonderful for scripts that don't change, but it's a nightmare if your inline code is dynamic:
// Generate hash for your inline script
const crypto = require('crypto');
const scriptContent = "console.log('Hello World');";
const hash = crypto.createHash('sha256')
.update(scriptContent)
.digest('base64');
// CSP header
`script-src 'self' 'sha256-${hash}'`I prefer nonces for most applications because they're more flexible and easier to implement with server-side rendering. Hashes make sense for static sites or specific scripts that never change.

CSP for React, Vue, and Angular: Framework-Specific Patterns
Here's where things get fascinating. Each major framework has its own quirks when it comes to CSP.
React applications typically bundle all JavaScript into external files, which makes CSP relatively straightforward. The main gotcha is if you're using {/* REMOVED: dangerouslySetInnerHTML */} or any analytics libraries that inject inline scripts. I realized that most React CSP issues come from third-party dependencies, not React itself.
Vue has a similar story, but you need to watch out for inline templates. If you're using the runtime-only build and compiling templates during the build step, you're golden. In other words, make sure you're not relying on template compilation in the browser.
Angular actually has built-in CSP support and will warn you about unsafe practices during development. When I finally decided to migrate an Angular app to strict CSP, the framework's TypeScript compiler caught most of the issues for me.
The common pattern across all frameworks: avoid eval(), Function(), and inline event handlers. Modern bundlers like Webpack and Vite make this easy by extracting everything into separate files.
Report-Only Mode: Testing CSP Without Breaking Your App
I cannot stress this enough! Always start with report-only mode. This lets you see what would be blocked without actually blocking it:
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy-Report-Only',
"default-src 'self'; " +
"script-src 'self'; " +
"report-uri https://yourdomain.com/csp-reports"
);
next();
});
// Report endpoint
app.post('/csp-reports', express.json(), (req, res) => {
console.log('CSP Violation:', req.body);
// Log to your monitoring service
res.status(204).end();
});I spent weeks in report-only mode on a production app, collecting violations and fixing them one by one. The violations showed me inline scripts I forgot about, third-party libraries loading unexpected resources, and even some Chrome extensions interfering with the page.
Common CSP Pitfalls and How to Debug Violations
Let me share the mistakes I've made so you don't have to:
Pitfall #1: Forgetting about favicons. Your CSP needs to allow your favicon source, or you'll see violations every page load.
Pitfall #2: Browser extensions. User extensions can inject scripts that trigger CSP violations. These aren't your fault, but they'll clutter your logs. Filter them out based on the blocked URI.
Pitfall #3: Third-party scripts loading other scripts. Google Analytics loads additional scripts dynamically. You need to allow those domains too, not just the initial script source.
Debugging CSP violations is straightforward once you know where to look. Open your browser's DevTools console—violations are logged with detailed information about what was blocked and which directive blocked it. I realized that most violations fall into a pattern: either you forgot to whitelist a legitimate resource, or you have actual security issues to fix.
Moving from Development to Production: A CSP Checklist
When I finally decided to ship strict CSP to production, I created this checklist:
- Run in report-only mode for at least a week
- Review all violation reports and categorize them
- Test thoroughly with different browsers (especially Safari and Firefox)
- Verify that your analytics and monitoring tools still work
- Have a rollback plan (easy with feature flags)
- Start with a permissive policy and gradually tighten it
- Monitor error rates closely for the first 48 hours
The ROI on implementing CSP properly is massive. You're preventing entire categories of attacks with a single HTTP header. Every XSS vulnerability that never happens saves you from incident response, customer notifications, and potential data breaches.
Start with default-src 'self' and script-src 'self'. Add report-uri or report-to for violation logging. Use nonces for any inline scripts you absolutely need. Luckily we can iterate from there based on real violations rather than guessing what resources we need.
And that concludes the end of this post! I hope you found this valuable and look out for more in the future!