Most websites fail a basic security check not because of some deep vulnerability, but because of a few HTTP response headers that were never set. They're some of the highest-leverage, lowest-effort wins in web security — usually one line of config each — and yet a huge share of sites ship without them.
Here are the five I see missing most often: what each does, why it matters, and the exact config to add it, with copy-paste blocks for nginx, Apache, Express, Vercel and Netlify at the end.
1. Strict-Transport-Security (HSTS)
What it does: tells browsers "only ever talk to me over HTTPS." After the first visit, the browser refuses to downgrade to HTTP even if a link or an attacker tries.
Recommended value:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Heads up: browsers remember this. Make sure HTTPS works on every subdomain before adding includeSubDomains, and know that preload is deliberately hard to undo.
2. Content-Security-Policy (CSP)
What it does: a whitelist of where the page may load scripts, styles and images. It's the single strongest defense against cross-site scripting (XSS).
Recommended starter:
Content-Security-Policy: default-src 'self'
This is the one header you cannot copy-paste blindly. A strict policy will break inline scripts, third-party widgets and analytics. Ship it in report-only mode first (Content-Security-Policy-Report-Only), watch what breaks, and loosen deliberately. It's worth the effort — it's the header that actually stops XSS.
3. X-Content-Type-Options
What it does: nosniff tells the browser to trust the declared Content-Type instead of guessing. Without it, a browser can interpret an uploaded file as JavaScript and execute it.
X-Content-Type-Options: nosniff
A pure freebie — one value, no downside. Set it.
4. X-Frame-Options (clickjacking)
What it does: controls whether your site can be embedded in an <iframe>. Without it, an attacker can load your site invisibly over theirs and trick users into clicking things they can't see.
X-Frame-Options: DENY
Use SAMEORIGIN if you embed your own pages. The modern equivalent is Content-Security-Policy: frame-ancestors 'none' — setting both is fine.
5. Referrer-Policy
What it does: controls how much of your URL is sent in the Referer header when a user clicks away. Full referrers can leak sensitive path or query data (tokens, IDs) to third parties.
Referrer-Policy: strict-origin-when-cross-origin
Bonus: Permissions-Policy
Locks down powerful browser features your site doesn't use:
Permissions-Policy: geolocation=(), camera=(), microphone=()
The copy-paste fix, per platform
nginx (inside your server block):
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Content-Security-Policy "default-src 'self'" always;
Apache (.htaccess or vhost, needs mod_headers):
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always set Content-Security-Policy "default-src 'self'"
Express (with helmet):
const helmet = require("helmet");
app.use(helmet({
strictTransportSecurity: { maxAge: 63072000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
contentSecurityPolicy: { directives: { defaultSrc: ["'self'"] } },
}));
// helmet sets X-Content-Type-Options and X-Frame-Options sensibly by default.
Vercel (vercel.json):
{ "headers": [ { "source": "/(.*)", "headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "geolocation=(), camera=(), microphone=()" },
{ "key": "Content-Security-Policy", "value": "default-src 'self'" }
] } ] }
Netlify (a _headers file in your publish directory):
/*
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=()
Content-Security-Policy: default-src 'self'
How to check your own site
- Command line:
curl -sI https://yoursite.comand read the headers. - From the browser: if you'd rather check any page — including staging or logged-in ones — and get the fix snippet inline, that's exactly what SurfaceWatch does: it grades the whole page A–F and hands you the config per platform. It's how I stopped re-Googling these.
Two honest caveats
- Headers are necessary, not sufficient. A perfect header score says nothing about authentication, access control, injection or your dependencies. This is the easy 20% that closes the most common gaps — not a clean bill of health.
- CSP needs tuning per app. Don't push
default-src 'self'to production without testing in report-only mode first, or you'll break your own scripts.
That's it — five headers, a handful of lines, and a meaningfully smaller attack surface. If you only touch one thing today, start with nosniff and HSTS (zero-risk), then take your time with CSP.