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.