Deploy React with Security Headers: Checklist
Before deploying a React app to production, your backend must emit a complete set of security headers. This article is a checklist: which headers to set, what values to use, how to test them, and how to avoid common deployment mistakes. A single missing header or misconfigured value can undo weeks of security work. This is your final verification step before going live.
In 2026, security.txt audits check for 12+ HTTP headers; 34% of production sites are missing at least one critical header (SecurityHeaders.com data, 2026).
Complete Security Headers Checklist
Set all of these headers on every response from your backend:
| Header | Purpose | Recommended Value |
|---|---|---|
| Content-Security-Policy | Prevent XSS via script injection | default-src 'self'; script-src 'self' 'nonce-<random>'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; object-src 'none'; report-to csp-endpoint |
| Strict-Transport-Security | Force HTTPS, prevent downgrade attacks | max-age=31536000; includeSubDomains; preload |
| X-Content-Type-Options | Prevent MIME-type sniffing | nosniff |
| X-Frame-Options | Prevent clickjacking | DENY (or SAMEORIGIN if you use iframes) |
| X-XSS-Protection | Legacy XSS protection (deprecated but harmless) | 1; mode=block |
| Referrer-Policy | Control which referrer info is leaked | strict-origin-when-cross-origin |
| Permissions-Policy | Restrict browser APIs | geolocation=(), microphone=(), camera=() |
| Access-Control-Allow-Origin | Allow cross-origin requests (if needed) | Specific origin (e.g., https://app.example.com), never * with credentials |
| Access-Control-Allow-Credentials | Allow cookies in cross-origin requests | true (only if using cookies) |
| Set-Cookie | Secure session cookies | SameSite=Strict; Secure; HttpOnly; Path=/; Max-Age=3600 |
Implementation: Complete Backend Middleware
Here is a production-ready middleware that sets all headers:
// Node.js/Express - security-headers.js
const crypto = require('crypto');
module.exports = function securityHeaders(app, config = {}) {
app.use((req, res, next) => {
// Generate CSP nonce for inline scripts
const nonce = crypto.randomBytes(16).toString('hex');
res.locals.nonce = nonce;
// 1. Content-Security-Policy
const cspDirectives = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"object-src 'none'",
"report-to csp-endpoint",
];
res.setHeader('Content-Security-Policy', cspDirectives.join('; '));
// 2. Strict-Transport-Security (HTTPS only; set max-age to 1 year)
if (req.protocol === 'https' || process.env.NODE_ENV === 'production') {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
// 3. X-Content-Type-Options
res.setHeader('X-Content-Type-Options', 'nosniff');
// 4. X-Frame-Options
res.setHeader('X-Frame-Options', 'DENY');
// 5. X-XSS-Protection (legacy but harmless)
res.setHeader('X-XSS-Protection', '1; mode=block');
// 6. Referrer-Policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// 7. Permissions-Policy (replaces Feature-Policy)
res.setHeader(
'Permissions-Policy',
'geolocation=(), microphone=(), camera=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()'
);
// 8. Access-Control-Allow-Origin (if cross-origin API)
const allowedOrigins = (config.allowedOrigins || []).concat([
'https://app.example.com',
]);
const requestOrigin = req.headers.origin;
if (allowedOrigins.includes(requestOrigin)) {
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token');
res.setHeader('Access-Control-Max-Age', '3600');
}
// 9. Reporting-Endpoints (for CSP violation reports)
res.setHeader(
'Reporting-Endpoints',
'csp-endpoint="https://example.com/csp-report"'
);
// 10. Additional security headers
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
next();
});
};
Use this middleware in your Express app:
// server.js
const express = require('express');
const securityHeaders = require('./security-headers');
const app = express();
// Apply security headers to all routes
securityHeaders(app, {
allowedOrigins: [
'https://app.example.com',
'https://mobile.example.com',
],
});
app.get('/', (req, res) => {
res.send('<h1>Secure React App</h1>');
});
app.listen(3001);
Testing Your Headers
Test 1: Use curl to inspect headers
curl -I https://example.com/
Expected output:
HTTP/2 200
content-security-policy: default-src 'self'; script-src 'self' 'nonce-...'; ...
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), microphone=(), camera=()
Test 2: Use online header checker
Visit https://securityheaders.com and enter your domain. It will scan all headers and grade your site (A+ is the target).
Test 3: Check CSP with browser DevTools
- Open your React app in the browser.
- Open DevTools (F12).
- Go to the Network tab.
- Click on any request.
- Check the Response Headers section.
- Verify
Content-Security-Policy,Strict-Transport-Security, and others are present.
Common Mistakes and Fixes
Mistake 1: Setting HSTS with a short max-age
// WRONG
res.setHeader('Strict-Transport-Security', 'max-age=3600'); // 1 hour (too short)
// RIGHT
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); // 1 year
Short HSTS max-age defeats the purpose. Set it to at least 6 months (15552000 seconds) for production.
Mistake 2: Setting HSTS on HTTP
// WRONG
res.setHeader('Strict-Transport-Security', 'max-age=31536000');
// This is sent over HTTP, which is insecure
// RIGHT
if (req.protocol === 'https') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
HSTS is meaningless over HTTP; only set it for HTTPS responses.
Mistake 3: Overly permissive CSP
// WRONG
res.setHeader('Content-Security-Policy', "default-src *; script-src 'unsafe-inline'");
// RIGHT
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'nonce-abc123'");
Overly permissive CSP opens your app to XSS. Use nonces instead of 'unsafe-inline'.
Mistake 4: Forgetting to allow API origins in CORS
// WRONG
res.setHeader('Access-Control-Allow-Origin', '*'); // Allows any origin
// RIGHT
const allowedOrigins = ['https://app.example.com', 'https://mobile.example.com'];
if (allowedOrigins.includes(req.headers.origin)) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
}
Be specific about which origins can access your API.
Mistake 5: Not rotating CSRF tokens
// WRONG - token stays the same for the session
app.post('/api/transfer', (req, res) => {
if (req.body.csrf_token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid token' });
}
// Process...
// Don't regenerate token
});
// RIGHT - regenerate after use
app.post('/api/transfer', (req, res) => {
if (req.body.csrf_token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid token' });
}
// Process...
req.session.csrfToken = crypto.randomBytes(32).toString('hex'); // Regenerate
res.json({ success: true });
});
Rotate tokens after each use to limit the window of exposure if a token is stolen.
Pre-Deployment Checklist
Before deploying your React app, verify:
☐ HTTPS is enabled (all requests are HTTPS)
☐ CSP header is set with nonces or hashes (no script-src 'unsafe-inline')
☐ Strict-Transport-Security is set with max-age ≥ 6 months and preload flag
☐ X-Content-Type-Options: nosniff is set
☐ X-Frame-Options: DENY is set (or SAMEORIGIN if using iframes)
☐ Referrer-Policy is set
☐ Session cookies have Secure, HttpOnly, and SameSite=Strict attributes
☐ CSRF tokens are generated and validated on all state-changing requests
☐ CORS headers are set with specific allowed origins (never * with credentials)
☐ CSP report-uri or report-to endpoint is configured
☐ Permissions-Policy is set to restrict unnecessary browser APIs
☐ All headers are tested with curl and securityheaders.com
☐ CSP violations are logged and monitored
☐ HTTPS certificate is valid and will not expire soon
☐ Backend dependencies are up-to-date (no known CVEs)
Key Takeaways
- Set all security headers on every response: CSP, HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy.
- Test headers with curl, online tools, and browser DevTools before deploying.
- Avoid common mistakes: short HSTS max-age, permissive CSP, overly open CORS, forgetting CSRF tokens.
- Rotate CSRF tokens after use for maximum security.
- Monitor CSP violations in production to catch XSS attempts and misconfigurations.
Frequently Asked Questions
Can I skip Strict-Transport-Security if I have HTTPS?
No. HSTS forces all future requests to use HTTPS, even if the user types http:// or follows an old link. Without HSTS, an attacker can downgrade the first request to HTTP (before the redirect to HTTPS). Set it to at least 6 months.
What is the difference between X-Frame-Options and frame-ancestors in CSP?
They serve the same purpose (prevent clickjacking). CSP frame-ancestors is the modern standard; X-Frame-Options is legacy but still widely used. Set both for broad compatibility.
Should I commit security headers to version control or set them via CI/CD?
Commit them to version control (in your backend code or config) so they are consistently applied across all environments. Use environment variables for origins and endpoints that differ between dev/staging/production.
What if securityheaders.com gives me a grade lower than A+?
Read the report. Missing headers like Referrer-Policy or Permissions-Policy lower the grade. Add them. If CSP is too permissive, tighten it. Aim for A+ before production.