SurfaceWatch
← All articles
HOW-TO·October 2026·7 min read

The 5 security headers your site is probably missing (and the exact fixes)

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

Two honest caveats

  1. 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.
  2. 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.

Check your headers in one click — add SurfaceWatch free