HTTP security headers are one of the highest-ROI security improvements you can make to a web app. They're a few lines of server config that defend against XSS, clickjacking, MIME sniffing, and protocol downgrade attacks. Yet most production apps still don't use them.
This guide covers the essential security headers, what they do, and what to actually set.
Content-Security-Policy (CSP)
CSP is the most powerful and most complex security header. It tells the browser which sources are allowed to load scripts, styles, images, and other resources — blocking inline scripts and unknown CDNs that attackers might inject.
A basic strict CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RANDOM_NONCE';
style-src 'self' 'nonce-RANDOM_NONCE';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
upgrade-insecure-requests;
Key directives:
-
default-src 'self'— baseline: only load from your own origin -
script-src 'nonce-...'— scripts need a per-request nonce attribute; blocks injected scripts -
frame-ancestors 'none'— prevents your site from being iframed (clickjacking defense) -
upgrade-insecure-requests— rewrites http:// to https:// for subresources
Nonces in practice:
// Express: generate nonce per request
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${res.locals.nonce}';`
);
next();
});
// Template: use the nonce
// <script nonce="<%= nonce %>">...</script>
Test mode before enforcing:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
This logs violations without blocking anything. Invaluable for finding what you'd break before switching to enforce mode.
Strict-Transport-Security (HSTS)
Tells the browser to only connect over HTTPS — even if the user types http://:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-
max-age=31536000— remember for 1 year -
includeSubDomains— applies to all subdomains -
preload— submit to browser preload lists so HTTPS is enforced before the first visit
Before enabling: Make sure your entire site (including all subdomains) works over HTTPS. HSTS with includeSubDomains is hard to undo — the browser won't connect over HTTP until the max-age expires.
Start with a short max-age (like 3600), verify everything works, then increase to 31536000.
X-Frame-Options
Prevents your pages from being embedded in iframes — clickjacking defense:
X-Frame-Options: DENY
Or allow only same-origin iframes:
X-Frame-Options: SAMEORIGIN
Note: frame-ancestors in CSP supersedes this header and is more flexible, but X-Frame-Options is still needed for older browsers.
X-Content-Type-Options
Prevents MIME type sniffing — a category of attack where the browser ignores your Content-Type and guesses the file type, potentially executing a file as JavaScript:
X-Content-Type-Options: nosniff
This should always be set. There's no downside.
Referrer-Policy
Controls how much of your URL is sent in the Referer header when users navigate away:
Referrer-Policy: strict-origin-when-cross-origin
Options from most to least permissive:
-
no-referrer-when-downgrade(browser default) — sends full URL unless going HTTP→HTTPS -
strict-origin-when-cross-origin— sends only origin for cross-origin, full URL for same-origin -
same-origin— sends full URL only for same-origin requests -
no-referrer— never sends referer
Use strict-origin-when-cross-origin for most apps. It prevents leaking paths/query strings (which may contain tokens or IDs) to third-party domains.
Permissions-Policy
Restricts which browser features your page can use — prevents malicious scripts from enabling the camera without your intent:
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
This disables camera, microphone, geolocation, and payment APIs for all origins. Enable them selectively:
Permissions-Policy: geolocation=(self "https://maps.example.com")
Cross-Origin Headers (COEP, COOP, CORP)
These headers enable cross-origin isolation, which is required for SharedArrayBuffer and high-resolution timers (mitigating Spectre-class attacks):
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Most apps don't need these unless they use SharedArrayBuffer. But if you use WebAssembly or audio worklets that need SharedArrayBuffer, these are required.
Complete Header Set
Express (Node.js)
Use the helmet package — it sets all of these sensibly:
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // relax if needed
imgSrc: ["'self'", "data:", "https:"],
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none';" always;
Apache
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=()"
Cloudflare / CDN
Transform rules let you add headers at the edge without touching your origin. In the Cloudflare dashboard: Rules → Transform Rules → Modify Response Header.
Testing Your Headers
securityheaders.com — grades your headers and lists what's missing.
Firefox DevTools: Network tab → select any request → Response Headers. You can see exactly which headers are present and their values.
curl:
curl -I https://yoursite.com
Priority Order
If you're adding these incrementally:
-
X-Content-Type-Options: nosniff— zero risk, immediate benefit -
X-Frame-Options: DENYorSAMEORIGIN Referrer-Policy: strict-origin-when-cross-origin-
Strict-Transport-Security— test with short max-age first -
CSPin report-only mode → then enforce -
Permissions-Policy— if your app doesn't use camera/mic/geo
CSP takes the most effort to get right but provides the most protection against XSS. Use report-only mode for weeks or months before switching to enforcement — you'll catch violations in the field that never showed up in testing.
Security headers are one of the few places where the protection-to-effort ratio is genuinely excellent. Add the quick ones now; work through CSP methodically.
Top comments (0)