Skip to main content

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:

  1. The server sends a Content-Security-Policy HTTP header listing allowed origins for scripts, styles, images, and other resources.
  2. The browser checks every resource against the policy before loading/executing it.
  3. 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 from https://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:

DirectivePurposeExample
script-srcTrusted sources for JavaScriptscript-src 'self' 'nonce-xyz'
style-srcTrusted sources for CSSstyle-src 'self' 'unsafe-inline'
img-srcTrusted sources for imagesimg-src 'self' data:
connect-srcTrusted sources for fetch/XHR/WebSocketconnect-src 'self' https://api.example.com
frame-srcTrusted sources for iframesframe-src 'none'
object-srcTrusted sources for object/embedobject-src 'none'
default-srcFallback for all unspecified directivesdefault-src 'self'
report-uriWhere to send CSP violationsreport-uri https://example.com/csp-report
report-toCSP v3 version of report-urireport-to my-csp-endpoint
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:

LimitationReasonWorkaround
Does not prevent all XSSCSP blocks execution; injected content might still render as textPair CSP with output escaping and sanitization
Performance overheadEvery resource is checked against the policyMinimal impact; browser caching helps
Requires server configurationCSP headers must be set by the backendNetlify, Vercel, and other platforms support CSP easily
Inline scripts need nonceCannot use <script> tags without nonce or hashUse nonce or move scripts to external files
DOM-based XSS not fully preventedIf JavaScript reads unsafe input and writes to DOM, CSP cannot prevent itStill 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?

  1. Use Content-Security-Policy-Report-Only to see violations without blocking.
  2. Check the browser console for CSP warnings.
  3. Monitor the /api/csp-report endpoint on your server.
  4. 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.

Further Reading