Eight HTTP security headers exist specifically to make browsers enforce protections against XSS, clickjacking, MIME sniffing, and data leaks. Most sites set them wrong — or don't set them at all. Here's what "wrong" actually looks like versus what you should be doing instead.
HTTP security headers are a browser-enforced layer of defense. When configured correctly, they tell the browser to block certain attacks automatically — without requiring application-level code changes. When configured wrong, they either do nothing or create operational problems. Most sites are in the second category.
Let's go through the eight headers that matter most, what "wrong" looks like, and how to get them right.
CSP is the strongest defense against cross-site scripting (XSS). It tells the browser which sources of content are allowed to execute on your page. If an attacker injects a script, CSP blocks it — assuming the policy is strict enough.
❌ Wrong — Too permissive
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; frame-ancestors 'self'
'unsafe-inline' allows inline scripts (including injected ones). A broad CDN trust chain means any compromise of that CDN breaks your CSP entirely. No report-uri means violations go unseen.
✓ Correct
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'sha256-abc123...'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; report-uri /__csp-report
Start with Content-Security-Policy-Report-Only in monitor mode to catch violations before enforcement. Enforce nonce-based or hash-based inline script allowlisting instead of 'unsafe-inline'.
Common CSP mistakes in 2026:
default-src * or default-src 'self' — too broad without specific overrides'unsafe-inline' in script-src — defeats the XSS protection purposescript-src https://* — allows any HTTPS CDN, not just yoursframe-ancestors — leaves clickjacking attack surfacereport-uri or report-to — violations are invisibleHSTS tells browsers to only connect to your site over HTTPS — no plain HTTP allowed, ever. It's the header that prevents SSL-stripping attacks on subsequent visits.
❌ Wrong — Max-age too short, no subdomains
Strict-Transport-Security: max-age=300
300 seconds is five minutes. That's essentially meaningless. After a 5-minute visit, the browser forgets HSTS is set and will happily allow an HTTP connection on the next visit. No includeSubDomains means subdomain visits aren't protected.
✓ Correct
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
63072000 seconds = 2 years. This is the minimum you want for real protection. includeSubDomains extends protection to every subdomain. preload submits your domain to the HSTS preload list (chrome://net-internals/#hsts) — but only do this when you're certain you never need HTTP on any subdomain.
X-Frame-Options prevents your site from being embedded in iframes — stopping clickjacking attacks. The problem: it's deprecated in favor of CSP's frame-ancestors, which is more capable.
❌ Wrong — Using X-Frame-Options
X-Frame-Options: DENY
X-Frame-Options works but is not supported in CSP Report-Only mode, can't be relaxed for specific origins, and isn't part of the Feature-Policy standard. If you need to allow specific domains to embed you (for integrations), you're stuck.
✓ Correct — Use CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'none' (blocks all embedding)
or for specific allowed origins:
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com
frame-ancestors replaces X-Frame-Options. Use it as part of your CSP policy. 'none' blocks all. 'self' allows your own domain. Specific origins allow controlled embedding.
This header tells browsers not to MIME-sniff responses. Without it, a browser might execute a response as HTML even when the server says it's JavaScript — enabling attacks where uploaded files are tricked into executing as scripts.
❌ Wrong — Header not set
(header absent entirely)
No X-Content-Type-Options header means browsers will MIME-sniff and might execute content types that don't match what the server declared. This is how some upload-based XSS attacks succeed.
✓ Correct
X-Content-Type-Options: nosniff
That's it. One value, one header. If the declared content type doesn't match what the browser thinks it should be, it's blocked from executing. Set it on all responses, not just HTML pages.
Every time your site links to an external site, the browser sends a Referrer header telling the destination where the user came from. If you don't control this, you're leaking full URL paths — including query parameters with IDs, tokens, and sensitive data — to third parties.
❌ Wrong — No policy or too loose
Referrer-Policy: no-referrer-when-downgrade (default, often leaky)
or no header at all (browser default varies)
no-referrer-when-downgrade sends the full URL when going from HTTPS to HTTPS (same-security). Many sites link to third parties on the same security level, leaking full path + query strings. No header at all means the browser's default kicks in — often no-referrer-when-downgrade or unsafe-url in some contexts.
✓ Correct
Referrer-Policy: strict-origin-when-cross-origin
Sends the full URL only for same-origin requests. For cross-origin requests, only sends the origin (scheme + host + port) — never the full path or query string. This prevents leaking sensitive URL parameters to third parties.
Permissions-Policy (formerly Feature-Policy) controls what browser features your page can access — camera, microphone, geolocation, payment handler, USB devices, web sharing, and more. If your site doesn't need these features, you should disable them.
❌ Wrong — Header not set
(all browser features remain accessible)
Without a Permissions-Policy, any page on your site can request camera, microphone, geolocation, and other sensitive APIs. Third-party scripts embedded on your page inherit these capabilities. If a script gets compromised, it has full access to whatever features you've left enabled.
✓ Correct — Disable what you don't need
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()
Empty parentheses disable the feature entirely. interest-cohort=() disables FLoC tracking. Only enable features where your application genuinely needs them — and only on the specific origins that need them.
COOP controls whether your browsing context can be opened by cross-origin documents. It's specifically designed to prevent popup-based attacks where a malicious site opens your site in a popup and uses the relationship to access your page's information.
❌ Wrong — Not set, or unsafe value
Cross-Origin-Opener-Policy: unsafe-origin-allow-popups
No COOP header means any cross-origin page can open your site in a popup and potentially access its browsing context group — enabling cross-origin attacks like Spectre. The unsafe-origin-allow-popups value undoes most of the protection.
✓ Correct
Cross-Origin-Opener-Policy: same-origin
same-origin ensures your browsing context is isolated from cross-origin documents. Only documents with the same origin can open your page in a way that shares the browsing context group. This prevents cross-origin attackers from using popup windows to reach your context.
CORP controls whether other origins can include your resources. It's a defense against side-channel attacks (like Spectre variants) and cross-origin resource loading attacks. It prevents other sites from loading your resources in ways that could leak their contents through speculative execution attacks.
❌ Wrong — Not set (allows all cross-origin loading)
(no CORP header — any site can embed your resources)
Without CORP, your resources can be loaded by any website via script tags, iframes, or other embedding mechanisms. While browsers have other protections, CORP adds a defense-in-depth layer specifically against side-channel attacks that exploit speculative execution.
✓ Correct
Cross-Origin-Resource-Policy: same-origin
or for resources that should be loadable by your own site only:
Cross-Origin-Resource-Policy: same-site
same-origin — only same-origin documents can load the resource. same-site — same-site origins can load it. cross-origin — explicitly allows everyone (use for CDN resources that need to be embedded). Choose based on what the resource actually needs.
Run this curl command against your site (or any site) to see all eight headers at once:
curl -sI https://example.com \
-H "Accept: text/html" \
| grep -iE "(content-security-policy|strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy|cross-origin-opener|cross-origin-resource)"
For a full response header dump including the values:
curl -sI https://example.com -H "Accept: text/html" \
| grep -iE "^(content-security-policy|strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy|cross-origin-opener|cross-origin-resource):"
Look for each of the eight headers. If one is missing, that's a gap. If the value looks permissive or uses a weak setting, that's a finding.
Most frameworks and web servers let you set security headers globally — no per-page code required. For Apache, add to your .htaccess or vhost config:
Header set Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'sha256-...'; img-src 'self' data:; frame-ancestors 'none'; report-uri /__csp-report"
Header set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Header set Cross-Origin-Opener-Policy "same-origin"
Header set Cross-Origin-Resource-Policy "same-origin"
For Nginx:
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; report-uri /__csp-report" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
EdgeIQ's HTTP Header Scanner analyzes all eight security headers on any domain, scores your configuration, and tells you exactly what's missing and what's set too loose — free, no signup required.
Scan your headers free →Security headers are a defense-in-depth layer — they don't fix broken application code, but they stop entire classes of attacks from working even when vulnerabilities exist. The eight headers above cover the most impactful protections: XSS prevention (CSP), protocol enforcement (HSTS), clickjacking prevention (frame-ancestors), MIME sniffing prevention (X-Content-Type-Options), referrer leakage prevention (Referrer-Policy), feature access control (Permissions-Policy), cross-origin context isolation (COOP), and side-channel attack mitigation (CORP).
Set them correctly. Monitor them with report-uri. Review them quarterly.