The HTTP Security Headers Every Site Should Set (and How to Audit Them)
A practical walkthrough of HSTS, CSP, nosniff, frame-ancestors, Referrer-Policy, and Permissions-Policy, what each header defends against, and how to audit a response.
The HTTP Security Headers Every Site Should Set (and How to Audit Them)
A browser trusts your server completely. Whatever your response says, the browser does, and a handful of HTTP response headers are the only place you get to say "treat this page carefully." Skip them and you leave a browser running with its guard down: it will downgrade to plain HTTP, run any script that finds its way onto the page, guess the type of files you serve, and let any other site load yours inside a hidden frame. None of that shows up as an error. The page works, the demo passes, and the gap sits there until someone finds it.
Six headers carry most of the weight. Each one closes off a specific, well-understood class of attack. Here is what they do and how to check that they are actually present on a real response.
HSTS: force HTTPS and stop the downgrade
Strict-Transport-Security tells the browser to only ever talk to your domain over HTTPS, for a set number of seconds. Once a browser has seen this header, it will refuse to make a plain HTTP request to you, even if a user types http:// or clicks an old link. That matters because the first request is where a network attacker strikes: intercept an http:// request on a coffee shop network, and you can strip the redirect to HTTPS before it ever happens. HSTS removes the window.
A solid value looks like Strict-Transport-Security: max-age=63072000; includeSubDomains; preload. The max-age is two years in seconds, includeSubDomains extends the rule to every subdomain, and preload makes you eligible for the browser's built-in list so even a first-ever visit is protected. The common mistake is a tiny max-age (a few hundred seconds) that someone set while testing and never raised, which gives almost no protection.
Content-Security-Policy: stop injected scripts from running
If HSTS protects the connection, Content-Security-Policy protects the page. CSP is an allowlist of where scripts, styles, images, and other resources may come from. The defense it gives you is against cross-site scripting: if an attacker slips a <script> into a comment field or a URL parameter, a strict CSP simply refuses to execute it, because the inline or remote source is not on the allowlist. It turns a stored XSS payload into an inert string.
CSP is the hardest header to get right because it has to match how your site actually loads things. A reasonable starting point for a site that serves its own assets:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
The two words to hunt for and remove are unsafe-inline and unsafe-eval. They reopen exactly the hole CSP is meant to close. The trap I see most often is a policy that started strict, then unsafe-inline got added to make one widget work, and nobody audited it again. The policy still looks long and serious in the response, but it no longer blocks anything.
nosniff, frame-ancestors, Referrer-Policy, Permissions-Policy
Three shorter headers round out the set, and each maps to a clear threat.
X-Content-Type-Options: nosniff stops the browser from guessing a file's type. Without it, a browser may decide that a file you served as plain text is "really" JavaScript and execute it. That MIME-sniffing behavior turns an innocent-looking upload or text endpoint into a script-execution path. The header has exactly one value and there is no reason not to send it.
Clickjacking is the next threat. An attacker loads your real page inside an invisible frame on their site, overlays it with their own buttons, and tricks a logged-in user into clicking something they cannot see. frame-ancestors 'none' in your CSP (or the older X-Frame-Options: DENY) blocks your page from being framed at all. If you need to allow your own subdomain to frame the page, use frame-ancestors 'self' instead.
Referrer-Policy controls how much of your URL gets sent to the next site a user visits. The default leaks the full path, which can carry tokens, internal IDs, or account hints in the query string. Referrer-Policy: strict-origin-when-cross-origin sends the full URL within your own site but only the bare origin to outside sites.
Permissions-Policy switches off browser features your site does not use, so a compromised script cannot reach for them. Permissions-Policy: camera=(), microphone=(), geolocation=() denies camera, microphone, and location access outright. Disabling what you never call shrinks the surface an injected script has to work with.
A recommended header set for a typical site
For a standard content or app site that serves its own assets over HTTPS, this is a defensible baseline. Adapt the CSP sources to match what you actually load.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Set-Cookie: session=...; Secure; HttpOnly; SameSite=Lax
The Set-Cookie line is part of the picture. Once you are behind HTTPS, every session cookie needs Secure (never sent over plain HTTP), HttpOnly (unreadable from JavaScript, so an XSS payload cannot steal it), and SameSite (limits cross-site sending, which blunts CSRF). The classic miss is moving a site to HTTPS but leaving cookies set the old way, so the flags never get added.
Auditing a response in practice
You cannot trust the config file alone, because a CDN, a reverse proxy, or a framework default may add, strip, or override headers before the response reaches the browser. The only honest check is to read the headers as they actually arrive.
Grab them with curl -I https://example.com (or copy the response headers from your browser's network panel) and read down the list. For each header above, ask: is it present, and is the value strong? Missing Strict-Transport-Security? The connection can be downgraded. Missing CSP, or one carrying unsafe-inline? Injected scripts will run. No nosniff? MIME sniffing is open. No frame-ancestors or X-Frame-Options? Clickjacking is possible. A Set-Cookie without Secure, HttpOnly, and SameSite? The session is exposed.
Doing that by eye on every release gets tedious and easy to skip, which is the whole reason I built the HTTP Security Header Auditor. I paste raw response headers or curl -I output into it, and it returns a checklist: which headers are present, which are missing, which cookies lack flags, and where infrastructure details like Server or X-Powered-By are leaking. It runs entirely in the browser and never sends a request to your site, so I can safely paste production headers that include real cookie names. The output comes out as Markdown, JSON, or CSV, which makes it easy to drop into an issue for the platform team. When I am already cleaning up a deploy, I usually pair it with the Env Secret Scanner to catch credentials that drifted into config at the same time.
Security headers are close to free to set and they fail silently when absent, which is exactly why they get forgotten. Pick the baseline above, deploy it, then audit the live response to confirm nothing in between quietly stripped it away. A two-minute check on every release is far cheaper than the alternative.
Made by Toolora · Updated 2026-06-13