Security Headers
Set the HTTP response headers that lock down the browser: CSP, HSTS, X-Frame-Options, and friends.
Why
Security headers are the browser-side controls that prevent entire classes of attacks with a few lines of configuration. Content-Security-Policy stops XSS by restricting where scripts can load from — even if an attacker injects a <script> tag, the browser refuses to execute it. HSTS forces HTTPS, preventing SSL-stripping downgrades. X-Frame-Options prevents clickjacking by forbidding your page from being framed. These headers are free, set once at the edge (Nginx, Cloudflare, or the framework middleware), and they protect every response without any code change. They are the highest-ROI security control: a few hours of configuration prevents attacks that would take weeks to fix in code.
When
Set security headers before the first production deploy and verify them on every staging deploy. Audit them whenever a new feature adds an external resource (a third-party script, a CDN font, an iframe embed).
How
1. Set Content-Security-Policy (CSP) — the most important header - Start in report-only mode: Content-Security-Policy-Report-Only - Collect violation reports, tune the policy until no legitimate traffic is blocked - Then enforce: script-src 'self' + specific trusted origins; no 'unsafe-inline' 2. Set the transport and framing headers - Strict-Transport-Security: max-age=63072000; includeSubDomains; preload - X-Frame-Options: DENY (or CSP frame-ancestors 'none' — the modern equivalent) - X-Content-Type-Options: nosniff (prevents MIME sniffing) 3. Set the privacy and referrer headers - Referrer-Policy: strict-origin-when-cross-origin (leak less to third parties) - Permissions-Policy: camera=(), microphone=(), geolocation=() (disable unused APIs) - Cross-Origin-Opener-Policy: same-origin (isolate browsing context) 4. Configure at the edge or in framework middleware - Nginx: add_header in the server block - Cloudflare: Transform Rules or Workers - Next.js: next.config.js headers() or middleware - Helmet (Express): app.use(helmet()) sets sane defaults 5. Verify with a scanner and a header-check tool - securityheaders.com or Mozilla Observatory — grade A+ target - ZAP passive scan flags missing/misconfigured headers - Test that CSP does not break legitimate functionality (third-party scripts, inline styles)
Example
# Nginx: /etc/nginx/sites-enabled/app.conf
server {
listen 443 ssl http2;
server_name example.com;
add_header Content-Security-Policy
"default-src 'self'; \
script-src 'self' https://plausible.io; \
style-src 'self' 'unsafe-inline'; \
img-src 'self' data: https:; \
connect-src 'self' https://api.example.com; \
frame-ancestors 'none'; \
base-uri 'self'; \
form-action 'self'" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" 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;
}
# Next.js: next.config.js
const cspHeader = [
"default-src 'self'",
"script-src 'self' https://plausible.io",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"frame-ancestors 'none'",
].join("; ");
module.exports = {
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "Content-Security-Policy", value: cspHeader },
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "X-Content-Type-Options", value: "nosniff" },
],
}];
},
};Common Problems
- ⚠Setting CSP to 'unsafe-inline' for script-src to avoid breaking inline scripts — this disables the XSS protection CSP is meant to provide, and you might as well not set the header at all.
- ⚠Setting HSTS with a short max-age (e.g. 3600 = 1 hour) — browsers forget the policy after an hour and become vulnerable to SSL-stripping again; use 63072000 (2 years) and include preload.
- ⚠Forgetting to set headers on error responses (404, 500) — Nginx add_header without always only sets on 2xx, so an error page is served without CSP and is exploitable.
- ⚠Setting X-Frame-Options: SAMEORIGIN when the app is never legitimately framed — DENY is stricter and prevents all framing, which is correct if you do not use frames at all.
Best Practices
- ✓Start CSP in Report-Only mode, collect violation reports for a week, and tune the policy until no legitimate traffic triggers violations — then enforce, so you do not break real users.
- ✓Use the 'always' flag (Nginx) or equivalent so headers are set on error responses too — an error page without CSP is an attack surface, and most misconfigurations are on non-2xx responses.
- ✓Use CSP frame-ancestors 'none' instead of X-Frame-Options (the modern equivalent that covers all framing, not just same-origin), and set both for defense-in-depth with older browsers.
- ✓Verify the headers with securityheaders.com or Mozilla Observatory after every deploy — a header that was set and then silently removed by a config change is a common regression.