HTTP Security Headers: A Deep Dive

HTTP security headers are one of the most cost-effective defenses in your stack. They require no code changes, they deploy at the infrastructure layer, and they meaningfully reduce the attack surface of every page you serve. Most mature applications have them — few have them well. This is a guide to getting them right.

Content-Security-Policy (CSP)

CSP is the heavyweight champion of HTTP security headers. It controls what resources the browser is allowed to load and execute on your page — scripts, stylesheets, fonts, images, frames, connect targets, everything. The goal is simple: even if an attacker injects markup into your page, the browser refuses to execute anything not explicitly permitted by your policy.

How it works

CSP uses directive:value syntax. Each directive controls a resource type. Multiple directives are separated by semicolons. A minimal CSP might look like:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self';

The browser enforces this at parse time. If a script tag doesn't have the matching nonce, it doesn't execute. If an inline event handler (onclick, onerror) is present, it doesn't fire. If a fetch() call goes somewhere you didn't allow, it's blocked.

Pros

CSP is the most effective defense against XSS that exists in the browser. Done properly, it reduces XSS to a non-issue because injected scripts simply won't run. It also gives you inventory control — you know exactly which third-party domains your frontend is allowed to communicate with. CSP reporting (report-to or report-uri) gives you visibility into blocked attempts, which is invaluable for catching攻击 in progress.

Cons

CSP is notoriously hard to get right. A single misconfigured directive can break your site — a missing 'unsafe-inline' when migrating, a subdomain you forgot to allow, a CDN you changed. The complexity grows with third-party integrations: analytics, ads, chat widgets, Tag Management systems all want to run scripts from their domains, and getting a complete allowlist takes time. nonce-based and hash-based CSP require server-side changes every time you add a new script tag. Legacy browser support is limited for some features. The reporting mechanism is under-utilised because most teams don't have a CSP report collection endpoint set up.

Config options

start: Use 'Content-Security-Policy-Report-Only' first — this runs the policy in monitor-only mode and sends reports without blocking anything. Deploy it for a few weeks before switching to enforce. Use nonces over hashes for scripts you control — nonces are easier to manage because you don't need to recompute a hash every time the script content changes. Keep 'unsafe-inline' as a last resort; if you must use it, pair it with a nonce so it's scoped. Whitelist specific domains, never '*'. If you use a CDN for assets, add it with 'https://cdn.example.com' (with scheme). Use report-to with a Reporting-Endpoints header pointing to your collection endpoint. Key directives:

default-src 'self'; script-src 'self' https://cdn.example.com 'nonce-INJECTED-SERVER-SIDE; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self';

frame-ancestors vs X-Frame-Options

frame-ancestors in CSP obsoletes X-Frame-Options. Use frame-ancestors 'none' to completely block embedding, or frame-ancestors 'self' to allow same-origin frames only. Note that frame-ancestors cannot be set in a meta tag — it only works as an HTTP response header.

Strict-Transport-Security (HSTS)

HSTS tells the browser: every future connection to this domain must use HTTPS. Once set, the browser refuses to make any HTTP request to your domain for the duration of max-age. It also refuses to proceed past certificate errors — there is no click-through on an invalid cert for an HSTS host.

How it works

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

On the first HTTPS visit, the browser records this domain as an HSTS host. On every subsequent visit — including the next two years (63072000 seconds) — it upgrades any HTTP URL to HTTPS before making the request. If the certificate is invalid, the connection is blocked entirely.

Pros

Eliminates the SSL-stripping class of attacks. Even if an attacker sits on the network and tries to intercept traffic by downgrading to HTTP, the browser refuses. Protects against MITM attacks on public WiFi. With includeSubDomains, the policy cascades to all subdomains, covering an entire surface at once. The preload directive gets you into the browser preload lists — Chrome, Firefox, Edge all ship HSTS preload lists compiled from hstspreload.org. This means the policy is active before the first-ever HTTPS visit, closing the window between first contact and first policy receipt.

Cons

Once HSTS is active with includeSubDomains, you cannot make any subdomain available over HTTP — even temporarily — for the duration of max-age. If you need to migrate a subdomain away from HTTPS, you must set max-age=0 first and wait for it to propagate. With preload, removal is slow — you must submit a removal request to hstspreload.org and wait for browser updates to ship. The initial HSTS bootstrap still requires at least one successful HTTPS connection — a sufficiently positioned attacker can intercept that first request before HSTS is known. Preloading mitigates this but requires meeting the inclusion criteria (max-age of at least 31536000, includeSubDomains, and preload directive).

Config options

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

start with one year (31536000s) and no preload while validating. After confirming all subdomains are HTTPS-capable, increase to two years and add preload for production. Only add preload after you are certain — removal is a multi-month process. Never set includeSubDomains unless every subdomain is HTTPS-served with a valid certificate.

X-Frame-Options (XFO)

X-Frame-Options: DENY

X-Frame-Options: SAMEORIGIN

XFO tells the browser whether your page is allowed to be embedded in a frame. It was introduced as a clickjacking defence and served that purpose well before CSP's frame-ancestors directive existed. Today it serves as a defence-in-depth measure and remains useful for older browsers that don't support CSP frame-ancestors.

Pros

One-line protection against clickjacking. DENY is maximally restrictive — no embedding whatsoever. SAMEORIGIN allows same-origin framing (useful for same-origin iframes like a CMS preview pane) while blocking cross-origin embedding. Simple, no ambiguity in what it does. Still worth setting even if you have CSP frame-ancestors — it's free defence-in-depth.

Cons

Has no effect on CSP frame-ancestors — the two are independent. ALLOW-FROM is obsolete and not supported by Chrome or Firefox — it only worked in old IE and is ignored by modern browsers. Only supports same-origin and deny — you cannot whitelist specific cross-origin domains. For complex embedding requirements, frame-ancestors in CSP is more flexible.

Config options

DENY if you never need to frame your content. SAMEORIGIN if you run same-origin framing (CMS previews, certain dashboards). Set in your web server config (nginx add_header, Apache Header set) or application middleware. Do not use ALLOW-FROM — it's dead. If you need cross-origin framing control, use CSP frame-ancestors instead.

X-Content-Type-Options

X-Content-Type-Options: nosniff

nosniff tells the browser to stop MIME-sniffing responses. By default, browsers inspect the content of a response to guess its type — a behaviour that can turn a served-as-text/plain file into executed HTML if the content looks like HTML. nosniff forces the browser to respect the Content-Type header you sent and not second-guess it.

Pros

Blocks a class of attacks where user-uploaded content (profile images, document uploads, file attachments) is served with an incorrect or missing Content-Type and the browser interprets it as executable HTML. Prevents reflection attacks where attacker-controlled content is stored and then served with a benign type but executed as HTML. One header, no complexity, works everywhere. Security scanners (like Mozilla Observatory) treat its absence as a finding.

Cons

If you have resources that are genuinely misconfigured — served with the wrong Content-Type — nosniff will break them. You must fix the Content-Type headers on your server. nosniff does not help if you actually intend to serve HTML content that has a text/plain type (don't do that). It has no effect on script execution if the Content-Type is correct — it's not a substitute for CSP.

Config options

Set it globally as X-Content-Type-Options: nosniff. Ensure all responses have a correct Content-Type header — especially application/json for APIs, text/css for stylesheets, application/javascript for scripts. If you serve user uploads, ensure your file server sets Content-Type correctly based on file extension or magic bytes, and pair this with CSP to limit what those resources can do if they're somehow executed.

Referrer-Policy

Referrer-Policy: strict-origin-when-cross-origin

Controls what information is sent in the Referer header when navigating away from your page. The Referer header carries the full URL of the previous page — including path, query string, and any sensitive data embedded in the URL. Referrer-Policy lets you reduce or eliminate this leakage.

How it works

The header accepts one value. The default (if none is set) is strict-origin-when-cross-origin in modern browsers. That means: on same-origin navigations, the full URL is sent; on cross-origin navigations, only the origin is sent; on downgrades (HTTPS to HTTP), no referrer is sent at all.

Pros

Prevents accidental leakage of sensitive URLs — search queries, session tokens in URLs, internal paths — to third parties. Protects user privacy when navigating from your HTTPS site to external links. strict-origin sends only the origin, not the full path, which is a good balance between functionality and privacy. no-referrer is the nuclear option — useful for highly sensitive applications where even the origin should not be disclosed.

Cons

Some analytics tools and affiliate tracking systems depend on the referrer being sent. If you set too restrictive a policy, you may lose referrer data in your analytics. The impact is usually acceptable — most analytics platforms have adapted to referrer restrictions. Do not use unsafe-url — it sends the full URL (including query strings) to any destination, which is a privacy and security liability.

Config options

Referrer-Policy: strict-origin-when-cross-origin

strict-origin-when-cross-origin is the recommended default — sends origin on cross-origin requests, full URL on same-origin. no-referrer is appropriate for APIs or pages handling highly sensitive data. You can also set this per-link using the referrerpolicy attribute on individual <a> tags, which is useful when you want different policies for different outbound links. You can also set it via the <meta> referrer tag, but the HTTP header has broader effect.

Cross-Origin-Resource-Policy (CORP)

Cross-Origin-Resource-Policy: same-origin

CORP tells the browser whether a resource may be loaded cross-origin. It was originally designed to prevent speculative execution side-channel attacks (Spectre/Meltdown) by restricting which origins can read a given resource, but it has broader utility as a resource isolation header.

Directives

Cross-Origin-Resource-Policy: same-origin | same-site | cross-origin

same-origin: only same-origin pages can load this resource. same-site: only pages from the same site (eTLD+1) can load it. cross-origin: any page can load it (this is the default if no CORP is set).

Pros

Prevents cross-origin pages from reading your resources via speculative execution side-channels. Allows you to lock down sensitive APIs or authentication endpoints so only your own origin can call them. Works at the fetch layer — the browser blocks the response before it reaches JavaScript. Provides protection beyond what CSP can offer, specifically for side-channel attack mitigation.

Cons

If you run a public API that you want anyone to fetch from via CORS, CORP same-origin or same-site will break it. You need to know which resources are intentionally public and which should be restricted. The default (cross-origin, effectively) means every resource without CORP set can be read by any cross-origin page. Legacy browsers don't support CORP. Using CORP requires you to explicitly opt-in for public resources.

Config options

For private resources (APIs, user data endpoints, authenticated content): Cross-Origin-Resource-Policy: same-origin. For resources shared across a site family but not publicly: Cross-Origin-Resource-Policy: same-site. For public CDN assets (fonts, images, JS libraries): don't set CORP, or explicitly set cross-origin so browsers can still fetch them. Test carefully — CORS and CORP interact in non-obvious ways.

Cross-Origin-Embedder-Policy (COEP)

Cross-Origin-Embedder-Policy: require-corp

COEP is a companion to COOP and is rarely set in isolation. It tells the browser: only load cross-origin resources that explicitly consent to being embedded via CORP or CORS. Without COEP, any cross-origin resource can be loaded in no-cors mode and its data may be accessible via side channels.

Directives

unsafe-none: load anything (default). require-corp: only load cross-origin resources that send Cross-Origin-Resource-Policy or use CORS. credentialless: like require-corp but strips credentials (cookies) from cross-origin no-cors requests.

Pros

When paired with Cross-Origin-Opener-Policy: same-origin, COEP enables cross-origin isolation — a prerequisite for SharedArrayBuffer, high-resolution timers (Performance.now() with unthrottled granularity), and the JS Self-Profiling API. Cross-origin isolation is increasingly required for performance-sensitive applications doing audio/video processing, gaming, or cryptographic operations in the browser. COEP also prevents your resources from being embedded by cross-origin pages that haven't explicitly opted in.

Cons

require-corp will break any cross-origin resource that doesn't send CORP or support CORS. This is a major constraint — popular third-party scripts, fonts, images, and CDNs all need to be audited for CORP/CORS support before you can enable it. Many third-party embeds (videos, maps, analytics iframes) will simply fail to load. credentialless is a less strict alternative that strips cookies but still requires CORP on cross-origin resources. The reporting API can help identify what breaks, but the migration effort is significant for established sites.

Config options

Enable COEP with Cross-Origin-Embedder-Policy: credentialless first — it's a gentler start since it strips cookies from cross-origin requests. Run in report-only mode with report-to to identify which cross-origin resources would be blocked. Once you've audited and updated third-party integrations, move to require-corp. Only enable if you need cross-origin isolation (SharedArrayBuffer, high-res timers). Most sites should not need this.

Cross-Origin-Opener-Policy (COOP)

Cross-Origin-Opener-Policy: same-origin-allow-popups

COOP controls whether your document can share a browsing context group with cross-origin documents. When a document opens a popup or is opened by another document, they may share a process in some browser implementations. COOP isolates your process so cross-origin documents cannot reach into it.

Directives

unsafe-none: share with any document (default). same-origin-allow-popups: same-origin documents and cross-origin documents opened via window.open() with same COOP value can share. same-origin: only same-origin documents with the same COOP value share the browsing context group. noopener-allow-popups: always open in a new browsing context group except when opened from a document with the same value.

Pros

Provides process-level isolation from cross-origin windows. Prevents cross-origin popup windows from retaining a reference to your page and accessing its DOM (via window.opener). Part of the cross-origin isolation story when you need SharedArrayBuffer or high-resolution timers. same-origin-allow-popups is a practical default that gets you isolation from arbitrary cross-origin pages while allowing legitimate popup integrations (OAuth flows, payment windows).

Cons

same-origin is very restrictive — it breaks most cross-origin popup scenarios including OAuth and third-party payment flows. same-origin-allow-popups is more workable but still requires careful testing. Cross-origin isolation via COOP + COEP is a major architectural commitment for sites with third-party integrations. Some legitimate cross-origin windows (embedded maps, third-party login) may be affected.

Config options

Cross-Origin-Opener-Policy: same-origin-allow-popups

Use same-origin-allow-popups as a practical middle ground if you need COOP for isolation. If you're building a security-critical application with no third-party popup needs, same-origin is appropriate. For most applications, the default unsafe-none is acceptable if COOP is not a stated goal.

Permissions-Policy

Permissions-Policy: camera=(); microphone=(); geolocation=(); payment=();

Permissions-Policy (the evolution of Feature-Policy) controls which browser features and APIs your page and its iframes are allowed to use. Unlike CSP which focuses on resource loading, Permissions-Policy governs access to powerful browser features — camera, microphone, geolocation, USB, Bluetooth, payment APIs, and more.

Pros

Reduces your attack surface by disabling browser features you don't use. Even if an XSS payload lands, it can't invoke getUserMedia() or access the Payment Request API if those features are disabled. Useful for containing the impact of a compromised iframe. Lets you express intent: your site doesn't use the camera, so the browser can hide or restrict access to it. Particularly valuable for embed scenarios — you can serve a widget in an iframe with a restrictive Permissions-Policy that prevents it from using features your embed doesn't need.

Cons

Requires careful auditing of which features your application actually uses. Set a feature to '()' (disabled) and an iframe you do control might break. Large, complex applications with many features need a detailed allowlist. Not all browsers support all directives. Unsupported directives are silently ignored, which is safe but means you don't get the restriction in those browsers. Requires coordination between frontend and backend — the policy is set via HTTP header but the features it controls are accessed via JavaScript APIs.

Config options

Start by auditing which features your application uses. For a typical web app: camera, microphone, geolocation, USB, Bluetooth, MIDI are often not used and should be disabled. For an analytics or marketing site, consider disabling most features. For iframes you control, use the allow attribute to grant specific features. For third-party iframes, Permissions-Policy on the parent controls what the iframe can do regardless of what the iframe's own headers say.

Permissions-Policy: accelerometer=(); ambient-light-sensor=(); autoplay=(); camera=(); clipboard-read=(); clipboard-write=(); geolocation=(); gyroscope=(); magnetometer=(); microphone=(); payment=(); usb=();

Pair this with a regular review — features you added six months ago may no longer be in use. The HTML attribute referrerpolicy on iframes is separate and can be set individually, but Permissions-Policy covers a broader set of features.

X-Permitted-Cross-Domain-Policies

X-Permitted-Cross-Domain-Policies: none

This header controls whether Adobe Flash and PDF readers can access your site's resources via cross-domain policy files. It matters only if you have Adobe Flash content or PDFs served from your domain that external clients might load — scenarios that are increasingly rare post-Flash deprecation.

Pros

Prevents malicious cross-domain policy files from being loaded from your domain by Flash or PDF clients. Since Flash is dead and modern browsers don't support it, this header is low-priority for most applications. Setting it to none is a safe default that causes no breakage. Security scanners still check for it, so setting it improves your security audit score.

Cons

Essentially irrelevant for modern web applications. Flash is deprecated, Silverlight is dead, PDF readers have tightened defaults. If you serve Adobe Flash content or host a cross-domain policy file intentionally, you need to understand the implications. For everyone else, it's a box-checking exercise.

Config options

X-Permitted-Cross-Domain-Policies: none

If you don't use Adobe Flash or host PDFs that need cross-domain access, set it and forget it. If you do have Flash content (why?), use master-only or by-content-type appropriately. There is no downside to setting none.

Cache-Control: No-Cache, No-Store, Private

Caching is a performance feature, but misconfigured caching is a security risk. Sensitive data — bank balances, medical records, personal information — should never be stored in shared caches (CDNs, proxies) or browser caches that persist beyond the session.

Directives

Cache-Control: no-store  # never cache

Cache-Control: no-cache  # revalidate every time

Cache-Control: private   # only browser caches, not shared proxies

no-store tells all caches: do not store this response under any circumstances. Every request must go back to the origin. no-cache tells caches: you can store this, but you must revalidate with the origin before using it on subsequent requests. private marks the response as user-specific — only the browser can cache it, not shared CDN or proxy caches.

Pros

Prevents sensitive data from leaking via shared caches — critical for banking, healthcare, e-commerce, and any authenticated application. no-store on API responses containing PII prevents proxies from storing user data. private is useful when you want the browser to cache performance-improving responses but don't want a CDN to cache user-specific content.

Cons

no-store disables all caching, which can significantly increase server load and latency for frequently accessed resources. Use it selectively for responses containing sensitive data, not globally. no-cache still requires a server round-trip, just without storing the response — less harmful for performance but still not ideal as a default. For static assets with long cache lifetimes, use content-addressable filenames (hashed URLs) instead — the cache is permanent but the URL changes on every deploy.

Config options

Cache-Control: no-store, private="*"

For authenticated pages and API responses containing personal data: Cache-Control: no-store, private. For static assets (JS, CSS, images): use content-hashed filenames and long max-age. For HTML pages: Cache-Control: no-cache or short max-age with must-revalidate. Never cache HTML pages with user-specific content at all.

Clear-Site-Data

Clear-Site-Data: cache, cookies

Clear-Site-Data tells the browser to delete browser-stored data associated with your origin — cookies, cache, storage, web SQL databases, service workers. It's the server-driven logout button.

Pros

Enables server-initiated data deletion — useful on logout, account deletion, or session termination. The browser handles the deletion, which is more reliable than client-side cookie clearing which may miss IndexedDB, Cache API, or service workers. Works for all storage types via the cache, cookies, storage, executionContexts, and * directives.

Cons

Only clears data for the origin that sends the header. If your static assets are on a CDN with a different origin, that data isn't cleared. Cannot clear data for origins you don't control. Not supported in all browsers — Safari has partial support. It clears data for the entire origin, not selective deletion of specific tokens or entries.

Config options

Clear-Site-Data: "cache", "cookies", "storage"

Send on logout endpoints and account deletion responses. Pair with a session invalidation on the server side — Clear-Site-Data is client-side deletion and doesn't revoke server-side sessions. If you have subdomains, each must send its own Clear-Site-Data header.

Expect-CT (Deprecated)

Expect-CT: max-age=86400, enforce, report-uri="https://report.example.com/ct"

Expect-CT was Chrome's mechanism for enforcing Certificate Transparency. It told browsers to reject certificates not logged to public CT logs. However, since June 2021, all newly issued publicly-trusted TLS certificates are required to support SCTs, making Expect-CT redundant. Chrome deprecated it from version 107 and it now has no effect in modern browsers.

Pros

None in 2026. If you're running it, you can remove it.

Cons

Header bloat. Deprecated. No effect in modern browsers. No reason to keep it.

Config options

Remove any existing Expect-CT headers. Certificate Transparency is now enforced by default in Chromium-based browsers. If you want CT enforcement guarantees, ensure your CA issues certificates with embedded SCTs (they do by default with Let's Encrypt and all major CAs). No action needed.

Summary Table

Deploy in this order for minimum disruption:

X-Content-Type-Options: nosniff

X-Frame-Options: DENY  (or use CSP frame-ancestors)

X-Permitted-Cross-Domain-Policies: none

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

Referrer-Policy: strict-origin-when-cross-origin

Permissions-Policy: camera=(); microphone=(); geolocation=(); ...

Content-Security-Policy: ... (deploy via Report-Only first)

Cross-Origin-Resource-Policy: same-origin  (for non-public resources)

Cross-Origin-Opener-Policy: same-origin-allow-popups  (if needed)

Cross-Origin-Embedder-Policy: credentialless  (if you need cross-origin isolation)

Test every change in staging. Use report-to endpoints for CSP, COEP, and COOP violations so you catch breakage before users do. Security headers are not a set-and-forget configuration — audit them quarterly and after any significant infrastructure changes.

The payoff for getting this right is substantial: an XSS that's blocked by CSP costs you nothing. An XSS that executes costs you everything.

Read more