Content Security Policy (CSP) is one of the most powerful — yet underused — browser security mechanisms available to web developers today. By defining exactly which resources your page is allowed to load, CSP effectively shuts down entire classes of attacks including Cross-Site Scripting (XSS), clickjacking, and data injection. In this hands-on guide, you’ll learn how to implement CSP headers from scratch, build a policy that works in production, and debug violations like a pro.
Why CSP Matters More Than Ever in 2026
XSS remains in the OWASP Top 10 year after year. Modern frameworks like React and Vue help by escaping output, but they don’t eliminate the risk entirely — especially when you use dangerouslySetInnerHTML, inline event handlers, or load third-party scripts. CSP acts as your last line of defense: even if an attacker injects a script tag, the browser refuses to execute it if it violates your policy.
With browser support now universal (including full Level 3 support in Chrome, Firefox, Safari, and Edge), there’s no reason to skip CSP in 2026.
How CSP Works: The Basics
CSP is delivered as an HTTP response header. The browser reads it and enforces rules about where scripts, styles, images, fonts, and other resources can be loaded from.
Here’s the simplest possible CSP header:
Content-Security-Policy: default-src 'self'This tells the browser: only load resources from the same origin as the page. Any inline scripts, external CDN links, or data URIs will be blocked.
Key Directives You Need to Know
default-src— Fallback for all resource typesscript-src— Controls JavaScript sourcesstyle-src— Controls CSS sourcesimg-src— Controls image sourcesconnect-src— Controls fetch, XHR, WebSocket endpointsfont-src— Controls web font sourcesframe-src— Controls iframe sourcesobject-src— Controls plugins (Flash, Java — set to'none')base-uri— Restricts the<base>tagform-action— Restricts where forms can submit to
Building a Real-World CSP Policy
Let’s build a CSP for a typical web app that uses Google Fonts, a CDN for scripts, and makes API calls to its own backend.
Step 1: Start with Report-Only Mode
Never deploy CSP in enforcement mode on day one. Start with Content-Security-Policy-Report-Only to see what would break:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reportStep 2: Define Your Policy
Here’s a production-ready policy for a modern web app:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net 'nonce-{RANDOM}';
style-src 'self' https://fonts.googleapis.com 'nonce-{RANDOM}';
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;Step 3: Implement Nonces for Inline Scripts
Instead of allowing 'unsafe-inline' (which defeats the purpose of CSP), use nonces. Generate a unique random value per request:
Node.js / Express example:
import crypto from 'crypto';
import express from 'express';
const app = express();
app.use((req, res, next) => {
// Generate a unique nonce for each request
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'"
].join('; '));
next();
});
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<script nonce="${res.locals.cspNonce}">
console.log('This inline script is allowed!');
</script>
</head>
<body><h1>CSP Protected Page</h1></body>
</html>
`);
});
app.listen(3000);Python / Django example:
# middleware.py
import secrets
class CSPMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
nonce = secrets.token_urlsafe(16)
request.csp_nonce = nonce
response = self.get_response(request)
csp = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; "
f"style-src 'self' 'nonce-{nonce}'; "
f"object-src 'none'; "
f"base-uri 'self'; "
f"frame-ancestors 'none'"
)
response['Content-Security-Policy'] = csp
return responseThen in your Django template:
<script nonce="{{ request.csp_nonce }}">
// Your inline JavaScript here
</script>Setting Up CSP Violation Reporting
CSP can send JSON reports when violations occur. This is invaluable for catching issues before they affect users:
// Express endpoint to collect CSP reports
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.warn('CSP Violation:', {
blockedURI: report['blocked-uri'],
violatedDirective: report['violated-directive'],
documentURI: report['document-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number']
});
res.status(204).end();
});Add the reporting directive to your header:
Content-Security-Policy: ...; report-uri /csp-report; report-to csp-endpointCommon CSP Pitfalls and How to Fix Them
1. Google Tag Manager / Analytics
GTM dynamically injects scripts, which clashes with strict CSP. Use strict-dynamic with a nonce:
script-src 'nonce-{RANDOM}' 'strict-dynamic'The 'strict-dynamic' keyword lets nonced scripts load additional scripts without listing every domain.
2. Inline Styles from JavaScript Libraries
Many UI libraries inject inline styles. Options:
- Use nonces for style tags (preferred)
- Use
'unsafe-hashes'with specific style hashes - As a last resort,
'unsafe-inline'forstyle-srconly (lower risk than script-src)
3. WebSocket Connections
Don’t forget to allow WebSocket URLs in connect-src:
connect-src 'self' wss://your-socket-server.comTesting Your CSP
Use these tools to validate your policy:
- Browser DevTools: The Console tab shows all CSP violations in real time
- Google CSP Evaluator: Paste your header for an instant security audit
- Observatory by Mozilla: Full security header scan at observatory.mozilla.org
- curl check: Verify headers are actually being sent:
curl -I https://yoursite.com | grep -i content-securityQuick CSP Checklist for Production
- Start in
Report-Onlymode for at least one week - Monitor violation reports and whitelist legitimate sources
- Never use
'unsafe-inline'forscript-src - Use nonces or hashes for inline scripts
- Set
object-src 'none'andbase-uri 'self'always - Add
frame-ancestors 'none'to prevent clickjacking - Include
upgrade-insecure-requeststo auto-upgrade HTTP to HTTPS - Switch to enforcement mode once violations are resolved
Wrapping Up
Content Security Policy is not optional in 2026 — it’s a fundamental part of shipping secure web applications. Start with report-only mode, iterate on your policy using real violation data, and graduate to enforcement once you’re confident. Combined with other headers like Strict-Transport-Security, X-Content-Type-Options, and Permissions-Policy, CSP forms the backbone of a defense-in-depth strategy that protects both your users and your reputation.
Got questions about implementing CSP in your stack? Drop a comment below — I’d love to help you lock things down.

Leave a Reply