Content Security Policy (CSP) is a critical security layer that helps prevent XSS, clickjacking, and other code injection attacks. Here’s how to properly implement CSP with React applications:
Basic CSP Setup for React
1. Minimal Production CSP
<!-- public/index.html -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
font-src 'self';
frame-ancestors 'none';
form-action 'self';">
2. Using Helmet in Express
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // Required for React
styleSrc: ["'self'", "'unsafe-inline'"], // Required for CSS-in-JS
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'"],
frameSrc: ["'none'"],
formAction: ["'self'"]
}
}));
React-Specific CSP Considerations
1. Handling Development Mode
// Dynamic CSP based on environment
const isDev = process.env.NODE_ENV === 'development';
const cspConfig = {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
isDev && "'unsafe-inline'",
isDev && "'unsafe-eval'",
"https://cdn.example.com"
].filter(Boolean),
// ... other directives
}
};
app.use(helmet.contentSecurityPolicy(cspConfig));
2. Webpack Configuration for CSP
// webpack.config.js
module.exports = {
// ... other config
devServer: {
headers: {
"Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' ws://localhost:*"
}
}
};
Advanced CSP Techniques
1. Nonce-Based CSP for Production
// Server-side nonce generation
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('hex');
next();
});
// Apply CSP with nonce
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.nonce}'`
],
// ... other directives
}
}));
// In your HTML template
<script nonce="<%= nonce %>">
// Inline scripts that need to execute
</script>
2. Hash-Based CSP
// Calculate SHA hash of your inline script
const crypto = require('crypto');
const fs = require('fs');
const script = fs.readFileSync('path/to/inline-script.js', 'utf8');
const hash = crypto.createHash('sha256').update(script).digest('base64');
// In your CSP
scriptSrc: [
"'self'",
`'sha256-${hash}'`
]
Common React Patterns with CSP
1. Styled Components/CSS-in-JS
// Required CSP directives
styleSrc: [
"'self'",
"'unsafe-inline'", // Required for most CSS-in-JS solutions
"https://fonts.googleapis.com"
]
2. External Libraries (Google Analytics, etc.)
scriptSrc: [
"'self'",
"https://www.google-analytics.com",
"https://www.googletagmanager.com"
],
connectSrc: [
"'self'",
"https://www.google-analytics.com"
],
imgSrc: [
"'self'",
"https://www.google-analytics.com",
"data:"
]
3. WebSocket Connections (for HMR)
// Development-only CSP addition
connectSrc: [
"'self'",
process.env.NODE_ENV === 'development' && "ws://localhost:*"
].filter(Boolean)
CSP Error Reporting
1. Report-Only Mode
// Good for initial rollout
app.use(helmet.contentSecurityPolicy({
directives: {
// ... your directives
},
reportOnly: true
}));
2. Violation Reporting Endpoint
directives: {
// ... other directives
reportUri: '/api/csp-violation',
reportTo: '/api/csp-violation'
}
// Handle reports
app.post('/api/csp-violation', (req, res) => {
console.warn('CSP violation:', req.body);
res.status(204).end();
});
Testing Your CSP
1. Browser DevTools
- Check Console for CSP violations
- Network tab shows CSP headers
2. Automated Testing
// Jest/Node test example
test('should have proper CSP header', async () => {
const response = await fetch('http://localhost:3000');
const cspHeader = response.headers.get('content-security-policy');
expect(cspHeader).toContain("default-src 'self'");
expect(cspHeader).toContain("frame-ancestors 'none'");
});
3. Online Validators
Migration Strategy
- Start with Report-Only Mode
- Monitor violations in production
- Gradually tighten policies
- Move to enforced CSP once stable
Troubleshooting Common Issues
1. React Errors Due to Strict CSP
// If you see errors like "Refused to evaluate a string as JavaScript"
// You may need to add:
scriptSrc: [
"'self'",
"'unsafe-inline'", // For React's required inline scripts
"'unsafe-eval'" // For webpack runtime
]
2. Styled Components Not Working
// Add this to your CSP:
styleSrc: [
"'self'",
"'unsafe-inline'", // Required for styled-components
"https://fonts.googleapis.com"
]
3. Font Loading Issues
fontSrc: [
"'self'",
"https://fonts.gstatic.com",
"data:" // For inline font data
]
Complete CSP Example for React
// Production-ready CSP configuration
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'none'"], // Default deny all
scriptSrc: [
"'self'",
"'unsafe-inline'", // Required for React
"'unsafe-eval'", // Required for webpack
"https://cdn.example.com"
],
styleSrc: [
"'self'",
"'unsafe-inline'", // Required for CSS-in-JS
"https://fonts.googleapis.com"
],
imgSrc: [
"'self'",
"data:",
"https://*.example.com"
],
connectSrc: [
"'self'",
"https://api.example.com",
"https://analytics.example.com"
],
fontSrc: [
"'self'",
"data:",
"https://fonts.gstatic.com"
],
frameSrc: ["'none'"], // Prevent clickjacking
formAction: ["'self'"],
baseUri: ["'self'"],
objectSrc: ["'none'"], // No Flash/plugins
reportUri: '/api/csp-violation'
},
reportOnly: false // Enforce CSP after testing
}));
Remember that implementing CSP is an iterative process – start with report-only mode, monitor violations, and gradually tighten your policy. The most secure CSP is one that balances security with actual functionality requirements.