HTTP Security Headers: A Practical Hardening Guide for CSP, HSTS, and Cross-Origin Isolation

Sep 6, 2026·
toolbox-editorial-team
· 10 min read
blog
Interactive Workbench LIVE

HTTP Security Headers: A Practical Hardening Guide for CSP, HSTS, and Cross-Origin Isolation

Initializing Workbench...
100% Client-Side RAM Sandbox
🔒 Private Execution: Zero server uploads.

Headers Are the Cheapest Security You Will Ever Deploy

Most web hardening work requires code changes, reviews, and regression testing. Security headers require a configuration line at the edge and cost nothing at runtime.

They are also the layer most often left at framework defaults — which is why a first audit of a mature application so often returns a grade of D or F.

The table below is the shortlist. Everything after it is detail.

HeaderBlocksPriority
Content-Security-PolicyCross-site scripting, data injectionCritical
Strict-Transport-SecurityProtocol downgrade, SSL strippingCritical
X-Content-Type-OptionsMIME-sniffing confusion attacksCritical
frame-ancestors / X-Frame-OptionsClickjacking, UI redressHigh
Referrer-PolicyURL and token leakage via RefererHigh
Permissions-PolicyUnwanted camera, mic, geolocation accessMedium
COOP / COEP / CORPCross-origin leaks; enables isolationSituational
Cache-Control: no-storeSensitive data cached on shared devicesSituational

Content-Security-Policy: The One That Does the Work

CSP tells the browser which sources of script, style, and other content are legitimate. Done properly, it turns a cross-site scripting bug from a full account takeover into a blocked console error.

The Modern Strict Policy

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  require-trusted-types-for 'script';

Each line earns its place:

  • 'nonce-{RANDOM}' — a fresh, cryptographically random value of at least 128 bits, generated per response and echoed on every legitimate <script nonce="...">. A reused or predictable nonce is no protection at all.
  • 'strict-dynamic' — lets a nonce-approved script load further scripts it creates programmatically, and causes host allowlists to be ignored. This matters because allowlist-based policies are routinely bypassable through JSONP endpoints and open redirects on allowlisted CDNs.
  • https: 'unsafe-inline' — a deliberate fallback for browsers too old to understand nonces. Modern browsers ignore 'unsafe-inline' whenever a nonce or hash is present, which is specified behaviour, so this line is backward compatibility rather than a hole.
  • object-src 'none' — removes the <object>, <embed>, and legacy plugin bypass surface. There is almost never a reason to allow it.
  • base-uri 'self' — prevents an injected <base> tag from redirecting every relative URL on the page to an attacker’s host.
  • frame-ancestors 'none' — the modern clickjacking control, superseding X-Frame-Options.

Roll It Out in Report-Only Mode First

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;

The Report-Only variant logs violations without blocking anything. Run it for a full traffic cycle — including whatever nightly and monthly jobs render pages — before switching to the enforcing header. Deploying an untested CSP straight to enforcement is the fastest way to take your own site down.

Directives That Do Not Behave as Expected

  • default-src is not a universal fallback. It does not cover base-uri, frame-ancestors, form-action, or sandbox. Set those explicitly.
  • 'unsafe-eval' re-enables eval and the string form of setTimeout. Some older bundlers and templating libraries need it; treat needing it as technical debt.
  • A meta http-equiv CSP silently drops frame-ancestors, report-uri, and sandbox, and only applies from the point the parser reads the tag. Send CSP as a real header.
Advertisement Sponsored

Strict-Transport-Security

HSTS (RFC 6797) instructs the browser to refuse plaintext HTTP for a host for a fixed period — closing the window in which a first plaintext request can be intercepted and stripped.

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
TokenMeaningNotes
max-ageSeconds to remember the HTTPS-only ruleRequired. 63072000 = two years
includeSubDomainsApplies to every subdomainVerify every subdomain serves HTTPS first
preloadRequests inclusion in browser preload listsNot part of RFC 6797; see hstspreload.org

Preload eligibility, per hstspreload.org, requires a valid certificate, an HTTP-to-HTTPS redirect on the same host, and a header on the HTTPS root with max-age of at least 31536000 plus both includeSubDomains and preload.

Two operational warnings:

  1. Preloading is hard to undo. Removal requests take effect only as browsers ship new versions, which can mean months. A subdomain that cannot serve HTTPS becomes unreachable for users whose browsers hold the preload entry.
  2. Ramp max-age upward. Start at 300, then a day, then a week, then a year. A mistake at max-age=63072000 is a two-year commitment for every visitor who received it.

HSTS is only sent over HTTPS. Browsers ignore it on a plaintext response, by design.


Clickjacking, Sniffing, and Referrer Leakage

X-Content-Type-Options: nosniff

One value, no options, no downside. It stops the browser from second-guessing your Content-Type, which is what turns an uploaded .txt file containing markup into stored XSS. Send it on every response.

X-Frame-Options

X-Frame-Options: DENY        # or SAMEORIGIN

Defined in RFC 7034 and superseded by CSP frame-ancestors. Note that the ALLOW-FROM value was never implemented by Chrome or Safari — to permit a specific external framer, you must use frame-ancestors https://partner.example. Sending both headers is common and harmless, as long as they do not contradict each other.

Referrer-Policy

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

This is now the browser default in Chrome and Firefox, but sending it explicitly documents the intent and protects against a proxy or framework overriding it. Cross-origin requests then carry only the origin — no path, no query string, no accidental token disclosure. Use no-referrer for pages whose URLs are themselves sensitive, such as password-reset links.

Permissions-Policy

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

An empty allowlist () denies the feature to the document and every frame it embeds. Deny what you do not use — a third-party script in an iframe cannot prompt for a camera it has no permission to request. This header replaces the older Feature-Policy, which used a different value syntax.


Cross-Origin Isolation: COOP, COEP, CORP

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
  • COOP same-origin severs the window.opener relationship with cross-origin pages, closing a family of tab-nabbing and cross-window attacks.
  • COEP require-corp requires every cross-origin subresource to opt in explicitly, via CORS or Cross-Origin-Resource-Policy.
  • CORP is the opt-in mechanism resources use to declare who may embed them.

COOP plus COEP together put the document in a cross-origin isolated state, which is a prerequisite for SharedArrayBuffer and high-resolution timers. That makes them mandatory for WebAssembly-heavy applications — and also the pair most likely to break third-party embeds. COOP alone is a safe, valuable default; adopt COEP only when you need isolation and have audited every embedded resource.


Headers to Delete

Hardening is subtraction as well as addition.

HeaderActionReason
X-XSS-ProtectionRemoveThe Chrome XSS Auditor it controlled was removed; the filter itself introduced information-leak bugs. OWASP advises against it.
X-Powered-ByRemoveDiscloses runtime and version, handing an attacker a CVE shortlist
ServerMinimiseApache/2.4.41 (Ubuntu) names the exact patch level to target
X-AspNet-VersionRemoveSame disclosure problem
Public-Key-PinsRemoveHPKP is obsolete and removed from browsers; use Certificate Transparency and CAA records

Also remember Cache-Control: no-store on any authenticated response. A shared or kiosk browser retaining an account page in its back-forward cache is a real disclosure, and no-cache alone does not prevent storage.


Server Configuration Pitfalls

Nginx: inheritance and error responses

add_header Content-Security-Policy "default-src 'self'" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Two behaviours cause almost every “header missing on some pages” report:

  1. add_header directives are not merged across levels. A single add_header inside a location block discards all inherited add_header directives from server and http. Repeat the full set in that block.
  2. Without always, headers are only added to successful responses. Your 404 and 500 pages ship unprotected — and those are exactly the pages most likely to reflect user input.

Apache

Header always set Content-Security-Policy "default-src 'self'"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always unset X-Powered-By

Header always set is the equivalent of Nginx’s always, covering error responses too.

A Note on CORS

Access-Control-Allow-Origin is not a hardening header — it relaxes the same-origin policy. And Access-Control-Allow-Origin: * cannot be combined with Access-Control-Allow-Credentials: true; browsers reject that pairing. Reflecting an arbitrary Origin header back while allowing credentials is a serious vulnerability, not a configuration convenience.


Step-by-Step: Auditing with Toolbox

  1. Capture your real headers. Run curl -sSI https://yoursite.example or copy the response headers from your browser’s Network panel.
  2. Open the tool: visit the Toolbox HTTP Header Security Auditor.
  3. Paste the raw response into the input box, or start from one of the built-in presets to see what a hardened and an unhardened response look like side by side.
  4. Read the Audit tab for the letter grade from A+ to F and the per-header findings behind it.
  5. Check the Table tab for the parsed name-value view, which is the quickest way to spot a duplicated or malformed header.
  6. Export from the Config tab — select Nginx or Apache and copy the generated hardening snippet straight into your server or edge configuration.
  7. Re-audit after deploying, and check a 404 as well as a 200. Missing headers on error responses is the single most common regression.

Parsing and grading happen entirely in your browser; the tool never fetches your site or transmits the headers you paste. That means you can safely audit a staging environment behind a VPN, or a response containing session cookies.

Two related hardening steps sit just outside the header layer: verifying that inbound webhooks are genuinely signed, covered in the HMAC-SHA256 signature guide, and validating token claims properly, covered in the JWT security guide.

FAQ

Frequently Asked Questions

Which HTTP security headers actually matter most?

Four carry most of the value. Content-Security-Policy limits which scripts can execute and is the primary defence against cross-site scripting. Strict-Transport-Security forces HTTPS and prevents downgrade attacks. X-Content-Type-Options with the value nosniff stops MIME confusion attacks. Either CSP frame-ancestors or X-Frame-Options prevents clickjacking. Referrer-Policy and Permissions-Policy are valuable but reduce data leakage rather than blocking an active attack.

Why is unsafe-inline ignored in my Content-Security-Policy?

That behaviour is specified, not a bug. When a directive contains a nonce or a hash source expression, the browser ignores unsafe-inline for that directive. The design intent is that a page can send unsafe-inline as a fallback for very old browsers while modern browsers enforce the stricter nonce-based policy. If you want inline scripts to run in a modern browser, you must give each one the matching nonce or hash.

What does HSTS preload require?

To be accepted onto the browser preload list at hstspreload.org, a site must serve a valid certificate, redirect all HTTP traffic to HTTPS on the same host, and send a Strict-Transport-Security header on the HTTPS root with a max-age of at least 31536000 seconds, plus the includeSubDomains and preload tokens. Preloading is difficult to reverse quickly, so confirm that every subdomain can serve HTTPS before submitting.

Is X-Frame-Options still needed if I use CSP frame-ancestors?

It is largely redundant in current browsers, which prefer frame-ancestors when both are present. Many organisations still send both, because X-Frame-Options remains the header that legacy scanners, corporate proxies, and older embedded browsers understand. Sending both is harmless provided they agree; a DENY value alongside a permissive frame-ancestors list is a configuration conflict waiting to confuse an audit.

Can I set security headers with an HTML meta tag instead?

Only partially. A Content-Security-Policy can be delivered via meta http-equiv, but the frame-ancestors, report-uri, and sandbox directives are ignored in that form, and the policy applies only after the parser reaches the tag. No other security header works as a meta tag at all. Send security headers from the server or the edge, where they cover every response including redirects and errors.