HTTP security headers that matter: CSP, HSTS, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy explained

Why HTTP response headers matter for security

Every HTTP response carries headers — metadata lines sent before the body that control how the browser handles the content. Most headers are prosaic (Content-Type, Cache-Control), but a specific set of security headers instructs the browser to enforce restrictions that block entire attack categories. These are not theoretical hardening measures; they are practical defences against attack classes that appear repeatedly in OWASP's Top 10 and CVE databases. Setting them costs nothing in performance — they add roughly 200–400 bytes to each response — and tools like securityheaders.com assign a letter grade to any live URL so you can see immediately which headers are missing.

The key insight is that security headers are enforced by the browser, not the server. The server announces a policy; the browser polices it. This means a header misconfiguration on a high-traffic site affects every visitor's browser simultaneously. Conversely, adding the right headers corrects years of accumulated risk in a single deployment. The five headers covered here — CSP, HSTS, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy — each target a different browser behaviour that attackers can exploit.

Content-Security-Policy: the XSS firewall

Cross-Site Scripting (XSS) is the most common web vulnerability in the OWASP Top 10. An attacker who can inject a <script> tag into your HTML can steal session cookies, redirect users, or exfiltrate form data. Content-Security-Policy (CSP) is a whitelist-based header that tells the browser which sources are trusted for scripts, styles, images, fonts, and other resource types. The directive Content-Security-Policy: default-src 'self' allows resources only from the same origin, blocking all external scripts and inline <script> blocks. A script-src directive overrides the default specifically for JavaScript.

The biggest CSP pitfall is the unsafe-inline keyword. Adding script-src 'unsafe-inline' re-enables inline scripts and completely defeats CSP's XSS protection, yet it is the most-reached-for workaround when a site breaks after enabling CSP. The correct alternative is nonces: each page load generates a cryptographically random value, included as script-src 'nonce-r4nd0m' in the header and as nonce="r4nd0m" on each legitimate <script> tag. An injected script without the matching nonce is blocked even if it appears inline. Browsers that do not support nonces fall back to the broader script-src policy. For teams not yet ready for a strict policy, Content-Security-Policy-Report-Only delivers violation reports to a collector endpoint without blocking anything — a safe way to measure the impact of a policy before enforcing it.

HTTP Strict Transport Security: preventing HTTPS downgrade attacks

SSL stripping is a downgrade attack demonstrated publicly at Black Hat 2009. An active on-path attacker intercepts a user's initial HTTP request (before any redirect to HTTPS) and proxies it as plain HTTP to the origin, while serving the encrypted connection back to the user. The user sees a padlock in some browsers, but the connection between the attacker and the server is the unencrypted one — credentials and session cookies are visible in plaintext. HTTP Strict Transport Security (HSTS) closes this window by instructing browsers to refuse HTTP connections for a domain entirely, for a period defined by the max-age directive in seconds.

A typical strong HSTS header is Strict-Transport-Security: max-age=31536000; includeSubDomains; preload. The max-age of 31536000 seconds is one year — the minimum required for inclusion in the HSTS preload list. includeSubDomains extends the policy to every subdomain. preload signals intent to be included in the browser-shipped preload list at hstspreload.org; once on that list, browsers will refuse HTTP connections even before they have ever visited the site and received the header. The critical constraint: HSTS must be delivered over a valid HTTPS response. A header sent over HTTP is ignored. Test before deploying includeSubDomains — if any subdomain lacks a valid TLS certificate, users will be locked out of it for the full max-age period.

X-Content-Type-Options and X-Frame-Options: two single-value wins

MIME sniffing is a legacy behaviour where some browsers inspect the first bytes of a response to guess its content type, overriding the Content-Type header. Internet Explorer was the most aggressive MIME sniffer; it would execute a .jpg as HTML if the first bytes looked like markup. An attacker who can upload content to your site (an image, a log file) can craft a payload that gets executed as a script when the browser sniffs it. X-Content-Type-Options: nosniff instructs every modern browser to trust the declared Content-Type and skip sniffing entirely. It has exactly one valid value — nosniff — and there is no reason not to set it on every response.

Clickjacking embeds your site inside a <iframe> on an attacker-controlled page, then overlays transparent buttons so users unknowingly click UI elements on your site. X-Frame-Options: DENY prevents your page from being framed by anyone; X-Frame-Options: SAMEORIGIN allows framing only by pages on the same origin. The ALLOW-FROM uri variant is obsolete and unsupported in current browsers. The modern equivalent is the CSP frame-ancestors directive (frame-ancestors 'none' maps to DENY, frame-ancestors 'self' maps to SAMEORIGIN), which additionally supports multiple allowed origins. Since not all browsers check frame-ancestors in all contexts, setting both X-Frame-Options and a frame-ancestors CSP directive gives defence in depth.

Referrer-Policy: controlling information leakage through the Referer header

The HTTP Referer header (spelled with one 'r' due to a historical typo in the RFC) sends the full URL of the page the user navigated from to every external resource that page loads — images, scripts, stylesheets, and the destinations of link clicks. If a logged-in user visits /account/reset?token=eyJ... and that page loads a third-party analytics script, the script's request carries the full URL including the reset token in the Referer header. Token leakage via referrer is a well-documented vulnerability class; GitHub patched a referrer-based token exposure as recently as 2020.

The Referrer-Policy header replaces the earlier Meta referrer mechanism and Referer header suppression workarounds. The policy strict-origin-when-cross-origin is the recommended default: it sends the full URL for same-origin requests (useful for internal analytics), but only the bare origin (https://example.com) for cross-origin requests, and nothing when navigating from HTTPS to HTTP. no-referrer sends nothing at all, maximising privacy but breaking any analytics that depend on referrer. unsafe-url always sends the full URL including path and query string — the browser default before 2020, and the source of most historical referrer leaks. Chrome, Firefox, and Safari all adopted strict-origin-when-cross-origin as their default in 2020–2021 if no policy header is present, but setting it explicitly ensures consistent behaviour across all browser versions.

A practical secure header baseline for any site

For a static site or simple web application, four headers cover the most common attack vectors with minimal configuration complexity: X-Content-Type-Options: nosniff (always, one line); X-Frame-Options: SAMEORIGIN (unless your site needs to be embedded legitimately); Referrer-Policy: strict-origin-when-cross-origin; and Strict-Transport-Security: max-age=31536000; includeSubDomains (once you have confirmed every subdomain is HTTPS). CSP requires more planning — start with Content-Security-Policy-Report-Only and a reporting endpoint, collect violations for a few days, then write a policy that allows only what you actually use.

Header configuration location depends on your stack: Nginx uses add_header directives in the server block; Apache uses Header always set in httpd.conf or .htaccess; Vercel, Netlify, and Cloudflare Pages each have a headers configuration file or dashboard setting. Most CDNs allow header injection at the edge, which is preferable to application-level headers because the CDN delivers them even for cached responses. The TeaFun HTTP Headers Checker tool fetches any public URL and displays which security headers are present, their values, and a quick-reference explanation of what each one does — use it to audit a site before and after deploying header changes.