iPGaze

Guide

HTTP Security Headers Explained: CSP, HSTS and More

A practical guide to HTTP security headers: what CSP, HSTS, X-Content-Type-Options, frame-ancestors, Referrer-Policy and Permissions-Policy do, with examples.

Every HTTP response your server sends carries a set of headers, and a handful of them act as instructions to the browser about how to behave securely on your site. Used well, security headers turn the browser itself into a second line of defence: they block injected scripts, force encrypted connections, stop your pages being framed by attackers, and limit what information leaks to third parties. They cost nothing in performance and are usually a few lines of server config, which makes them one of the highest-return security improvements available. You can see exactly which headers a site sends today with the Security Headers tool, or inspect the full raw response with the HTTP Headers tool.

This guide walks through the headers that matter, what each one does, and example values you can adapt. None of them replace fixing vulnerabilities in your application, but together they shrink the blast radius when something does go wrong.

Content-Security-Policy (CSP)

Content-Security-Policy is the most powerful and the most finicky of the security headers. It tells the browser which sources of content (scripts, styles, images, fonts, frames) are allowed to load and execute, which is the single most effective defence against cross-site scripting (XSS). If an attacker manages to inject a script tag into your page, a strict CSP simply refuses to run it because its source is not on the allowlist.

A reasonable starting policy looks like this: 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 default-src directive sets the fallback, and each more specific directive overrides it for that resource type. Avoid 'unsafe-inline' and 'unsafe-eval' for scripts wherever you can, since they re-open the very hole CSP is meant to close; modern policies instead use a per-request nonce or a hash to permit specific trusted inline scripts.

CSP is easy to get wrong and break your own site, so it ships with a safe rollout mode. Send Content-Security-Policy-Report-Only with the same policy and the browser will report violations (optionally to a report-to or report-uri endpoint) without actually blocking anything. Run in report-only for a week or two, watch what would have broken, tighten the policy, and only then switch to the enforcing header.

Strict-Transport-Security (HSTS)

Strict-Transport-Security tells the browser to only ever connect to your site over HTTPS, even if the user types http:// or clicks an old insecure link. Once a browser has seen this header, it upgrades every request to your domain to HTTPS automatically and refuses to let users click through certificate warnings. This closes the window where a first plain-HTTP request could be intercepted and redirected before the upgrade happens.

A typical value is Strict-Transport-Security: max-age=31536000; includeSubDomains; preload. The max-age is in seconds (here one year) and tells the browser how long to remember the rule. includeSubDomains applies the policy to every subdomain, so only enable it once you are certain every subdomain serves HTTPS correctly. The preload directive signals that you want your domain baked into browsers' built-in HSTS lists, after you submit it at hstspreload.org; preload is effectively irreversible in the short term, so make sure your TLS is solid first by checking your certificate with the SSL / TLS Check tool.

A practical tip: start with a short max-age such as 300 seconds while you confirm everything works on HTTPS, then ramp it up to a year. HSTS only takes effect over a valid HTTPS connection, so a broken certificate will simply prevent the header from ever being honoured.

X-Content-Type-Options: nosniff

X-Content-Type-Options has exactly one useful value: X-Content-Type-Options: nosniff. It stops the browser from trying to guess ("sniff") the content type of a response and instead forces it to trust the declared Content-Type header. Without it, a browser might decide that a file you serve as plain text or an image actually looks like JavaScript or HTML and execute it, which is a real attack vector when users can upload files to your domain.

There is no downside and no tuning involved, so this header should be on every response your server sends. It is the simplest security header to deploy and one of the most commonly missing, so it is an easy win to verify and fix.

X-Frame-Options vs frame-ancestors

Clickjacking is an attack where your site is loaded inside an invisible iframe on a malicious page, tricking users into clicking buttons they cannot see. The old defence is X-Frame-Options: DENY (never allow framing) or X-Frame-Options: SAMEORIGIN (only your own pages may frame you). It works in every browser but is limited: it cannot express an allowlist of multiple specific domains, and the long-deprecated ALLOW-FROM value is unreliable.

The modern replacement lives inside CSP: the frame-ancestors directive. frame-ancestors 'none' is equivalent to DENY, frame-ancestors 'self' is equivalent to SAMEORIGIN, and frame-ancestors 'self' https://partner.example permits specific trusted domains to frame you. Where both headers are present, modern browsers obey frame-ancestors and ignore X-Frame-Options, so the common belt-and-braces approach is to send X-Frame-Options: SAMEORIGIN for legacy clients alongside a CSP frame-ancestors directive for everything current.

Referrer-Policy

When a user clicks a link or your page loads a resource, the browser normally sends a Referer header telling the destination which URL the request came from. That can leak sensitive information, since full URLs sometimes contain session tokens, search terms, or internal paths you would rather not hand to third-party analytics, ad networks, or external sites.

Referrer-Policy lets you control how much of that is shared. A sensible default is Referrer-Policy: strict-origin-when-cross-origin, which sends the full URL to your own origin, sends only the origin (scheme and domain, no path) to other HTTPS sites, and sends nothing at all when downgrading from HTTPS to HTTP. If you want to be stricter, Referrer-Policy: no-referrer sends nothing ever, at the cost of breaking analytics that rely on referrers. This header has no real performance impact and is a quiet privacy and security improvement.

Permissions-Policy

Permissions-Policy (formerly Feature-Policy) lets you switch off browser features that your site does not use, so that injected or compromised code cannot abuse them. It governs powerful capabilities such as the camera, microphone, geolocation, USB access, and payment APIs. Disabling what you do not need means that even if an attacker runs script on your page, they still cannot, for example, silently request the user's location.

A lock-everything-down example is Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), where the empty parentheses mean "no origin is allowed to use this feature." To permit a feature only for your own site you would write geolocation=(self), and to allow a specific embedded third party you list its origin. Enumerate the features your application genuinely uses, allow just those, and deny the rest.

A Note on X-XSS-Protection

You will still see X-XSS-Protection recommended in older articles, but it is deprecated and you should not rely on it. It controlled a legacy XSS auditor built into some browsers that has since been removed from Chrome and Edge and was never implemented by Firefox, and in some configurations it actually introduced vulnerabilities of its own. The current best practice is to send X-XSS-Protection: 0 to explicitly disable any lingering buggy behaviour, and to rely on a strong Content-Security-Policy as your real XSS defence instead. Spending effort tuning the old auditor is wasted; spend it on CSP.

Deployment, Testing and Grading

Deploy headers in a safe order. Start with the zero-risk ones that need no tuning: X-Content-Type-Options: nosniff, a Referrer-Policy, and X-Frame-Options. Add HSTS next, but begin with a short max-age and only add includeSubDomains and preload once you have confirmed every host serves HTTPS correctly, which you can validate with the SSL / TLS Check tool. Save CSP for last and roll it out in report-only mode first, because it is the one most likely to break legitimate functionality.

Test after every change. Confirm headers are actually being sent (a misconfigured proxy or CDN can strip them) by inspecting the live response with the HTTP Headers tool, and make sure your redirect chain is clean so the headers reach the final page rather than only an intermediate hop, which you can trace with the Website Status tool. Remember that headers must be present on every response, not just the homepage, and that error pages and API endpoints are frequently overlooked.

To grade your overall posture, run the Security Headers tool, which checks for each header described here and flags what is missing or weak. Treat the result as a checklist rather than a contest: a perfect score with a broken CSP that you loosened to 'unsafe-inline' is worse than a slightly lower score with a genuinely restrictive policy. Aim for present, correct, and as strict as your application can tolerate.

Tools mentioned in this guide