Skip to main content

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:

HeaderPurposeRecommended Value
Content-Security-PolicyPrevent XSS via script injectiondefault-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-SecurityForce HTTPS, prevent downgrade attacksmax-age=31536000; includeSubDomains; preload
X-Content-Type-OptionsPrevent MIME-type sniffingnosniff
X-Frame-OptionsPrevent clickjackingDENY (or SAMEORIGIN if you use iframes)
X-XSS-ProtectionLegacy XSS protection (deprecated but harmless)1; mode=block
Referrer-PolicyControl which referrer info is leakedstrict-origin-when-cross-origin
Permissions-PolicyRestrict browser APIsgeolocation=(), microphone=(), camera=()
Access-Control-Allow-OriginAllow cross-origin requests (if needed)Specific origin (e.g., https://app.example.com), never * with credentials
Access-Control-Allow-CredentialsAllow cookies in cross-origin requeststrue (only if using cookies)
Set-CookieSecure session cookiesSameSite=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

  1. Open your React app in the browser.
  2. Open DevTools (F12).
  3. Go to the Network tab.
  4. Click on any request.
  5. Check the Response Headers section.
  6. 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.

Further Reading