HTTP security headers explained: CSP, HSTS, X-Frame-Options, and what to set in 2026.
What each HTTP security header does, the value to set, and the mistakes scanners flag most. Includes a copy-paste baseline for Nginx, Next.js, and Express.
HTTP security headers are response headers a web server sends to instruct the browser to enforce restrictions on the page: only connect over HTTPS, only run scripts from these origins, never render inside another site's frame, never guess a file's type. They work because the browser enforces them, so a single correct header can block an entire attack class even when the application has a bug that would otherwise allow it. They are also the easiest high-value fix in web security: most are one line of configuration.
This guide explains each header, the value to set in 2026, the mistake we see most often when scanning, and how to test the result. For the broader checklist a header check belongs to, see How to check if a website is secure.
Which security headers should every site send?
| Header | Protects against | Recommended value | Most common mistake |
|---|---|---|---|
| Strict-Transport-Security | Protocol downgrade, cookie theft over HTTP | max-age=31536000; includeSubDomains; preload | Missing, or max-age too short for preload |
| Content-Security-Policy | Cross-site scripting, data injection, malicious third-party scripts | Site-specific allowlist; default-src 'self' | 'unsafe-inline' in script-src, or no policy |
| X-Content-Type-Options | MIME sniffing attacks | nosniff | Missing on static assets |
| X-Frame-Options / frame-ancestors | Clickjacking | DENY, or CSP frame-ancestors 'none' | Only one of the two set; inconsistent across pages |
| Referrer-Policy | Leaking URLs with tokens to third parties | strict-origin-when-cross-origin | Missing, so full URLs leak by default in some cases |
| Permissions-Policy | Abuse of camera, microphone, geolocation, and other APIs | Deny everything not used | Missing |
| Cross-Origin-Opener-Policy | Cross-window attacks, Spectre-class leaks | same-origin | Rarely set; needed for isolation |
| Set-Cookie attributes | Session theft, CSRF | Secure; HttpOnly; SameSite=Lax | Session cookie without HttpOnly |
What does Strict-Transport-Security (HSTS) do?
HSTS tells the browser to use HTTPS for every future request to this host for the stated number of seconds, and to refuse to connect if the certificate is invalid. It closes the window where a user types example.com, the first request goes over HTTP, and an attacker on the network intercepts it. With includeSubDomains the rule covers every subdomain; with preload and a submission to the preload list, browsers enforce it before the first visit.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadCommon mistake: setting it with a short max-age (the preload list requires at least one year, 31536000 seconds) or setting it only on the apex and not on www, or vice versa. Both hosts need it. Test with curl -sI https://example.com | grep -i strict.
What does Content-Security-Policy (CSP) do?
CSP is an allowlist of where the page may load scripts, styles, images, fonts, frames, and connections from. If an attacker injects a <script> tag into the page, the browser refuses to run it because the inline script is not on the list. It is the strongest defence against cross-site scripting, and the hardest header to get right, because the policy has to match everything the page legitimately uses.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
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';
object-src 'none';
upgrade-insecure-requestsCommon mistakes: 'unsafe-inline' or 'unsafe-eval' in script-src, which disables most of the protection; a wildcard * in script-src or connect-src; and no policy at all. Frameworks that inline a hydration script often push teams toward 'unsafe-inline'. The better path is a per-response nonce or hash, which modern frameworks support. If you cannot get there in one step, deploy Content-Security-Policy-Report-Only first and tighten from the reports.
What does X-Content-Type-Options do?
X-Content-Type-Options: nosniff stops the browser from guessing a response's type from its content. Without it, a file uploaded as an image but containing HTML can be rendered as HTML, and a text response can be executed as a script. There is exactly one value and no reason not to send it on every response, including static assets, which is where scanners most often find it missing.
What do X-Frame-Options and frame-ancestors do?
Both prevent clickjacking, where an attacker loads your page invisibly inside their own and tricks the user into clicking. X-Frame-Options: DENY is the legacy header and is still honoured everywhere. Content-Security-Policy: frame-ancestors 'none' is the modern replacement and takes precedence where both are set. Send both. If you legitimately embed your page in a partner site, list that origin in frame-ancestors rather than removing the protection.
What does Referrer-Policy do?
The Referer header tells the next site where the user came from, including the full URL, which may contain session tokens, reset links, or private identifiers. Referrer-Policy: strict-origin-when-cross-origin sends the full URL to your own origin, only the origin to other HTTPS sites, and nothing to HTTP sites. It is the browser default in modern versions, but setting it explicitly covers older clients and makes the intent auditable.
What does Permissions-Policy do?
Permissions-Policy (formerly Feature-Policy) controls which browser features the page and its embedded frames may use. A marketing site has no reason to access the camera, microphone, or geolocation. Denying them means a compromised third-party script cannot request them either.
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()What about Cross-Origin-Opener-Policy and friends?
Cross-Origin-Opener-Policy: same-origin isolates your window from pages that opened it, blocking a class of cross-window attacks. Cross-Origin-Resource-Policy controls who may load your resources, and Cross-Origin-Embedder-Policy is required for features like SharedArrayBuffer. They are less universally applicable than the six above. Set COOP on everything; add the other two when you need cross-origin isolation.
Which headers are deprecated?
- X-XSS-Protection. The browser filter it controlled has been removed from every major browser and in some versions introduced vulnerabilities. Either omit it or send
X-XSS-Protection: 0. Scanners that still demand1; mode=blockare out of date. - Public-Key-Pins (HPKP). Removed from browsers; do not set it.
- Expect-CT. Certificate transparency is now enforced by default; the header is obsolete.
Copy-paste baseline: Nginx
add_header Strict-Transport-Security "max-age=31536000; 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 "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" always;The always flag matters: without it Nginx omits the headers on error responses, which is exactly where scanners look.
Copy-paste baseline: Next.js
// next.config.mjs
const headers = [
{ key: "Strict-Transport-Security", value: "max-age=31536000; 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: "camera=(), microphone=(), geolocation=(), payment=()" },
{ key: "Content-Security-Policy", value: "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" },
];
export default {
async headers() {
return [{ source: "/:path*", headers }];
},
};Copy-paste baseline: Express
import helmet from "helmet";
app.use(helmet({
strictTransportSecurity: { maxAge: 31536000, includeSubDomains: true, preload: true },
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
},
},
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
}));How do you test security headers?
Request several paths, not just the home page: an error page, a static asset, an API route, and an authenticated page. Headers set at the application layer are frequently missing from responses served by the CDN, the error handler, or a different service on a subdomain.
for p in / /nonexistent-404 /static/app.js /api/health; do
echo "== $p"; curl -sI "https://example.com$p" | grep -i -E "strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy"
donePythorix samples multiple paths per host before reporting a header as missing, which is one of the false-positive classes we describe in Scoring real breach risk. A free scan checks every host on your domain, including the subdomains you forgot.
Frequently asked questions
What are the most important HTTP security headers?
Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options or CSP frame-ancestors, Referrer-Policy, and Permissions-Policy. Together they enforce HTTPS, block injected scripts, prevent MIME sniffing and clickjacking, limit referrer leakage, and deny unused browser features.
Is X-XSS-Protection still needed?
No. Modern browsers removed the filter it controlled. Omit the header or set it to 0. Scanners that flag its absence are using outdated rules.
What is the correct HSTS value?
max-age=31536000; includeSubDomains; preload. One year is the minimum for the browser preload list. Set it on both the apex domain and www, and only after confirming every subdomain serves valid HTTPS.
Why does my site still fail a header scan after I added the headers?
Usually because the headers are set on the main application but not on error pages, static assets served by a CDN, redirects, or a service on a subdomain. Check several paths and every host, and in Nginx use the always flag.
Can Content-Security-Policy break my site?
Yes, if the policy omits an origin the page actually uses. Deploy it as Content-Security-Policy-Report-Only first, collect violation reports, then switch to enforcing once the reports are clean.