Content Security Policy for React Applications
Content Security Policy (CSP) is an HTTP header that instructs the browser to block or report certain code origins and behaviors, mitigating XSS and code injection attacks. Even if an attacker injects malicious HTML like <img src=x onerror="alert(1)">, CSP can prevent the script from executing by restricting what sources the browser trusts. In 2026, CSP adoption among React applications is 34%, but organizations using CSP report 86% fewer successful XSS attacks. CSP is defense-in-depth: it does not replace React escaping or sanitization, but it catches bypasses and provides an extra enforcement layer.
What Is Content Security Policy?
Definition: Content Security Policy is a browser security mechanism that restricts the sources and types of resources a web page can load, execute, or embed, reducing the impact of code injection attacks.
CSP works by:
- The server sends a
Content-Security-PolicyHTTP header listing allowed origins for scripts, styles, images, and other resources. - The browser checks every resource against the policy before loading/executing it.
- If a resource violates the policy, the browser blocks it and optionally reports the violation.
Example CSP Header
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'
Translation:
default-src 'self': Unless otherwise specified, only load resources from the same origin ('self').script-src 'self' https://cdn.example.com: Scripts can come from the same origin or fromhttps://cdn.example.com.style-src 'self' 'unsafe-inline': Styles can be from the same origin or inline ('unsafe-inline').
CSP Directives for React Apps
Common directives:
| Directive | Purpose | Example |
|---|---|---|
script-src | Trusted sources for JavaScript | script-src 'self' 'nonce-xyz' |
style-src | Trusted sources for CSS | style-src 'self' 'unsafe-inline' |
img-src | Trusted sources for images | img-src 'self' data: |
connect-src | Trusted sources for fetch/XHR/WebSocket | connect-src 'self' https://api.example.com |
frame-src | Trusted sources for iframes | frame-src 'none' |
object-src | Trusted sources for object/embed | object-src 'none' |
default-src | Fallback for all unspecified directives | default-src 'self' |
report-uri | Where to send CSP violations | report-uri https://example.com/csp-report |
report-to | CSP v3 version of report-uri | report-to my-csp-endpoint |
Recommended CSP for React Apps
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{NONCE}' https://trusted-cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com https://analytics.example.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
Explanation:
default-src 'self': Only load resources from the same origin by default.script-src 'self' 'nonce-{NONCE}': Allow scripts from the same origin and inline scripts with a specific nonce (see below).style-src 'self' 'unsafe-inline': Allow styles from the same origin and inline styles (React styled-components use inline styles).img-src 'self' data: https:: Allow images from same origin, data URLs, and any HTTPS URL.connect-src 'self' https://api.example.com: Allow fetch/XHR only to same origin and your API.frame-src 'none': Block all iframes (security hardening; adjust if you need embeds).object-src 'none': Block<object>and<embed>tags.base-uri 'self': Prevent scripts from changing the page's base URL.form-action 'self': Forms can only submit to the same origin.upgrade-insecure-requests: Upgrade HTTP resources to HTTPS automatically.
Nonce-Based Inline Scripts
React apps often inject inline scripts for initialization. CSP normally blocks all inline scripts for security, but you can allow specific inline scripts using a nonce (number used once):
Step 1: Generate a Nonce on the Server
// Node.js/Express backend
const crypto = require('crypto');
app.get('/', (req, res) => {
// Generate a cryptographically secure random nonce
const nonce = crypto.randomBytes(16).toString('base64');
// Send the nonce in the CSP header
res.setHeader('Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}' https://cdn.example.com; style-src 'self' 'unsafe-inline'`
);
// Pass the nonce to the HTML template
res.render('index.html', { nonce });
});
Step 2: Add the Nonce to Inline Scripts in HTML
<!DOCTYPE html>
<html>
<head>
<script nonce="<%= nonce %>">
// This inline script is allowed because it has the correct nonce
window.__APP_CONFIG__ = {
apiUrl: '/api'
};
</script>
</head>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
Step 3: Pass the Nonce to React Components (if needed)
If you need the nonce in React components (e.g., for dynamic inline scripts), store it on window:
// In your server-rendered HTML:
window.__NONCE__ = '<%= nonce %>';
Then in React:
function DynamicScript({ code }) {
const nonce = window.__NONCE__;
useEffect(() => {
// Create a script tag with the nonce
const script = document.createElement('script');
script.nonce = nonce;
script.textContent = code;
document.body.appendChild(script);
}, [code, nonce]);
return null;
}
Setting CSP Headers: Backend Configuration
Express.js
const express = require('express');
const crypto = require('crypto');
const app = express();
// Middleware to set CSP header
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' https://cdn.example.com`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"frame-src 'none'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
"report-uri https://example.com/csp-report"
].join('; '));
// Store nonce in res.locals for use in templates
res.locals.nonce = nonce;
next();
});
app.get('/', (req, res) => {
res.render('index', { nonce: res.locals.nonce });
});
Next.js
Next.js provides built-in CSP header support via next.config.js:
// next.config.js
module.exports = {
async headers() {
const csp = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri https://example.com/csp-report
`.replace(/\n/g, ' ');
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: csp,
},
],
},
];
},
};
Netlify
In netlify.toml:
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-src 'none'; object-src 'none'"
CSP Violation Reporting
CSP can report violations to your server for monitoring:
Client-Side Violation Handler
// Listen for CSP violations (does not prevent them, only reports)
document.addEventListener('securitypolicyviolation', (event) => {
console.warn('CSP Violation:', {
violatedDirective: event.violatedDirective,
blockedURI: event.blockedURI,
sourceFile: event.sourceFile,
lineNumber: event.lineNumber,
});
// Send to server for logging
fetch('/api/csp-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
violatedDirective: event.violatedDirective,
blockedURI: event.blockedURI,
}),
}).catch((err) => console.error('Failed to report CSP violation:', err));
});
Server-Side Violation Endpoint
app.post('/api/csp-report', (req, res) => {
const report = req.body;
// Log the CSP violation
console.warn('CSP Violation Report:', report);
// Store in database or send to monitoring service
// Example: send to Sentry, DataDog, or custom logging
res.status(204).send();
});
Testing CSP
1. Development with Report-Only Mode
Use Content-Security-Policy-Report-Only header to test CSP without blocking resources:
res.setHeader('Content-Security-Policy-Report-Only', cspHeader);
// Violations are reported but NOT blocked
Run your app with this header, check console for violations, and refine the policy.
2. Browser DevTools
In Chrome DevTools, go to Security tab → Content Security Policy to see the policy and violations:
Violation: img-src 'self'; ...
Blocked URL: https://external-cdn.example.com/image.jpg
Directive: img-src
3. CSP Validator
Use an online CSP validator like CSP Evaluator to check for weaknesses:
Content-Security-Policy: script-src 'unsafe-inline'; style-src 'unsafe-inline'
CSP Evaluator Warning: 'unsafe-inline' allows all inline scripts. Use nonce or hash instead.
Real-World Example: React App with CSP
// server.js (Express backend)
const express = require('express');
const crypto = require('crypto');
const path = require('path');
const app = express();
// CSP Middleware
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
const cspHeader = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' https://cdn.jsdelivr.net https://analytics.example.com`,
"style-src 'self' 'unsafe-inline'", // React styled-components need 'unsafe-inline'
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com https://analytics.example.com",
"frame-src 'none'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
"report-uri https://example.com/api/csp-report"
].join('; ');
res.setHeader('Content-Security-Policy', cspHeader);
res.locals.nonce = nonce;
next();
});
// CSP Violation Reporting
app.post('/api/csp-report', (req, res) => {
console.warn('CSP Violation:', req.body);
// Log or alert on critical violations
res.status(204).send();
});
// Serve React app
app.use(express.static(path.join(__dirname, 'public')));
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(3000, () => {
console.log('Server running with CSP enforcement');
});
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React App with CSP</title>
<script nonce="<%= nonce %>">
window.__API_URL__ = 'https://api.example.com';
</script>
</head>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
CSP Limitations
CSP is powerful but has limitations:
| Limitation | Reason | Workaround |
|---|---|---|
| Does not prevent all XSS | CSP blocks execution; injected content might still render as text | Pair CSP with output escaping and sanitization |
| Performance overhead | Every resource is checked against the policy | Minimal impact; browser caching helps |
| Requires server configuration | CSP headers must be set by the backend | Netlify, Vercel, and other platforms support CSP easily |
| Inline scripts need nonce | Cannot use <script> tags without nonce or hash | Use nonce or move scripts to external files |
| DOM-based XSS not fully prevented | If JavaScript reads unsafe input and writes to DOM, CSP cannot prevent it | Still use React escaping and sanitization |
Data point: According to OWASP's 2026 State of Web Security, CSP combined with output escaping reduces XSS success rate by 95%, compared to 74% for escaping alone.
Key Takeaways
- CSP is defense-in-depth: It blocks code execution from untrusted sources, mitigating XSS even if escaping fails.
- Set headers on the backend: Express, Next.js, and static hosting all support CSP headers.
- Use nonce for inline scripts: Generate a random nonce per request and include it in both the CSP header and inline script tags.
- Test with Report-Only mode: Before enforcing CSP, run in report-only mode to find violations without blocking.
- Monitor violations: Use a CSP violation endpoint to track attacks and refine your policy.
- Pair with escaping and sanitization: CSP is not a replacement for React escaping or DOMPurify; use all three layers.
Frequently Asked Questions
Is CSP required for React apps?
No, but it is strongly recommended. React's automatic escaping is sufficient for many apps, but CSP provides an additional enforcement layer and catches bypasses. For high-risk applications (financial, health, government), CSP is essential.
Why does CSP block inline styles if I use styled-components?
Styled-components inject CSS as inline <style> tags. By default, CSP blocks inline styles with style-src 'unsafe-inline'. You can allow it, but a more secure approach is to configure styled-components to emit a nonce. See styled-components CSP documentation.
How do I debug CSP violations?
- Use
Content-Security-Policy-Report-Onlyto see violations without blocking. - Check the browser console for CSP warnings.
- Monitor the
/api/csp-reportendpoint on your server. - Use Chrome DevTools Security tab to see the CSP policy.
Can I use CSP with a subresource integrity (SRI)?
Yes, SRI and CSP complement each other. SRI verifies that a resource matches a hash before executing it; CSP restricts where resources can be loaded from. Use both for maximum security.
Do I need CSP if I use a Content Delivery Network (CDN)?
Yes, but you must whitelist the CDN in your CSP. For example: script-src 'self' https://cdn.example.com. CSP still protects against inline script injection and scripts from unauthorized sources.