Quick answer
Security headers are the first line of defense against XSS, clickjacking, MITM and info leaks. Minimal audit in 2026: CSP (with frame-ancestors + no unsafe-inline), HSTS (preload + includeSubDomains), X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Cookies Secure + HttpOnly + SameSite=Lax, an explicit Permissions-Policy. Header-only reports are usually info/low (€50-€200), but as a chain with another bug they can raise the severity 2 levels.
Full table — headers to check
| Header | Recommended value | What it prevents | Severity if missing |
|---|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | MITM / HTTPS downgrade | Medium |
Content-Security-Policy | default-src 'self'; frame-ancestors 'none'; ... | XSS, clickjacking, exfil | Medium-High |
X-Content-Type-Options | nosniff | MIME confusion XSS | Low |
X-Frame-Options | DENY or SAMEORIGIN | Clickjacking (legacy) | Low (if CSP frame-ancestors exists) |
Referrer-Policy | strict-origin-when-cross-origin | Token leak via Referer | Low-Medium |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Abuse of sensitive APIs | Low |
Cross-Origin-Opener-Policy | same-origin | Spectre, window.opener | Low-Medium |
Cross-Origin-Embedder-Policy | require-corp | Spectre, side-channel | Low |
Cross-Origin-Resource-Policy | same-origin | Cross-origin leaks | Low |
Cache-Control (on auth endpoints) | no-store, private | Data leak via cache | Medium |
Content-Security-Policy — the king header
CSP is the most complex and the most impactful. A poorly built CSP lets every XSS through; a well-built CSP blocks even reflected XSS.
Example of a correct CSP (modern SPA)
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RANDOM123' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://api.target.com wss://ws.target.com;
frame-src https://js.stripe.com https://hooks.stripe.com;
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
object-src 'none';
upgrade-insecure-requests;
report-uri /csp-report
Mistakes that kill the CSP
| Anti-pattern | Why it's bad |
|---|---|
script-src 'unsafe-inline' | Allows inline <script> → any reflected XSS passes |
script-src 'unsafe-eval' | eval(), Function(), setTimeout(string) → gadget chains |
script-src https: | Allows scripts from any HTTPS → protects nothing |
script-src *.cdn.com | If the CDN serves user content (JSONP) → instant bypass |
No frame-ancestors | Clickjacking |
No base-uri | <base> injection → changes the origin of relative scripts |
No form-action | Form hijacking in XSS to exfil credentials |
default-src 'self' without object-src 'none' | <object data="data:..."> executes legacy Flash |
[!danger] frame-ancestors is NOT enough
frame-ancestors 'none'prevents traditional iframing, but it does not prevent window.open() popups with clickjacking via UI confusion. To close the full vector: addCross-Origin-Opener-Policy: same-origin+ awindow.top === window.selfcheck in critical JS.
Test the CSP
# See the header
curl -sI https://target.com | grep -i content-security
# Google's official evaluator
# Paste the CSP into https://csp-evaluator.withgoogle.com/
# Search for bypasses based on the whitelisted domains
# https://github.com/PortSwigger/csp-bypass
HSTS — the simple but critical header
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| Directive | Meaning |
|---|---|
max-age=31536000 | 1 year in seconds. How long the browser forces HTTPS |
includeSubDomains | Applies to all subdomains. Critical to prevent attacks via http subdomain takeover |
preload | Enrollment in the Chrome/Firefox HSTS preload list (hardcoded) |
Preload list
Enroll at hstspreload.org. Requirements:
- Serve HSTS with
max-age >= 31536000,includeSubDomains,preload. - Redirect HTTP → HTTPS on root + on all subdomains.
- Support HTTPS on all subdomains (including
mail.target.com).
Cookies — the flags that count
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
| Flag | What it does | When it's critical |
|---|---|---|
Secure | Only over HTTPS | Always. Without it = leak via downgrade |
HttpOnly | Inaccessible from JS | Sessions, auth tokens — always |
SameSite=Lax | Not sent in cross-site POSTs | Sensible default. Protects CSRF on mutations |
SameSite=Strict | Not sent in ANY cross-site request | For maximum-sensitivity tokens (banking) |
SameSite=None | Sent cross-site (requires Secure) | Third-party widget cookies (analytics, embeds) |
__Host- prefix | Forces Secure, Path=/, no Domain | Defense in depth for auth |
__Secure- prefix | Forces Secure | For auth/session cookies |
[!tip] SameSite Lax vs Strict in 2026 Chrome and Firefox already default to
SameSite=Laxif not specified. But there are still endpoints in old frameworks that set it without an explicit SameSite → reportable. Strict breaks OAuth/SSO flows because the cookie doesn't arrive after the redirect → Lax is the sweet spot.
Referrer-Policy — the silent leak
Referrer-Policy: strict-origin-when-cross-origin
Without this header, the browser sends the full Referer (including path + query) to any external site you link to. Result: password reset tokens, session IDs in the query, OAuth codes — all leaked to third parties.
| Value | When |
|---|---|
no-referrer | Maximum privacy. For sensitive apps (medical, financial) |
strict-origin-when-cross-origin | Sensible default in 2026. Full origin same-origin, only origin cross-origin, nothing cross-origin http→https |
same-origin | Only same-origin sends the referrer, cross-origin sends nothing |
unsafe-url | NEVER. Always sends the full URL |
Reportable
URLs with tokens in the query (?reset_token=, ?session=, ?code=) + no restrictive Referrer-Policy → when the user clicks an external link, the token leaks to the third party. Medium severity if the token is still valid.
Permissions-Policy — the modern one (formerly Feature-Policy)
It used to be Feature-Policy. Renamed to Permissions-Policy in 2020+ and it supports more APIs.
Permissions-Policy:
camera=(),
microphone=(),
geolocation=(),
payment=(self),
usb=(),
accelerometer=(),
gyroscope=(),
magnetometer=(),
interest-cohort=()
camera=()→ no origin can access it.camera=(self)→ only same-origin.camera=(self "https://trusted.com")→ same-origin + that specific one.interest-cohort=()→ opt out of FLoC/Topics API tracking.
Reportable if the app embedded in an iframe can activate mic/camera/geolocation without the user expecting it → defense in depth.
COEP / COOP / CORP — cross-origin isolation
Needed to use SharedArrayBuffer, high-precision performance.now(), and to mitigate Spectre/Meltdown.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
- COOP
same-origin: your window doesn't share a browsing context with cross-origin popups → closeswindow.openerattacks. - COEP
require-corp: all loaded resources must haveCross-Origin-Resource-Policyor CORS. Enables "cross-origin isolation". - CORP
same-origin: your resources can only be loaded from same-origin.
[!info] When to enable them If your app uses SharedArrayBuffer (WebAssembly, threading), you need COOP+COEP. If your app does NOT use them, COOP
same-originis a free win to close window.opener attacks. COEP breaks many third-party apps — implement carefully.
How to test it all at once
Tools
# Quick CLI
curl -sI https://target.com
# Online — the de facto standard
# https://securityheaders.com/?q=target.com
# https://observatory.mozilla.org/analyze/target.com
# CSP specific
# https://csp-evaluator.withgoogle.com/
Manual audit script
#!/bin/bash
URL="$1"
echo "=== Headers for $URL ==="
curl -sI "$URL" | grep -iE "(strict-transport|content-security|x-content-type|x-frame|referrer-policy|permissions-policy|cross-origin|set-cookie)"
echo -e "\n=== Cookies analysis ==="
curl -sI "$URL" | grep -i "set-cookie:" | while read -r line; do
name=$(echo "$line" | sed 's/.*Set-Cookie: \([^=]*\)=.*/\1/i')
flags=""
echo "$line" | grep -qi "HttpOnly" || flags+="[NO HttpOnly] "
echo "$line" | grep -qi "Secure" || flags+="[NO Secure] "
echo "$line" | grep -qi "SameSite" || flags+="[NO SameSite] "
[ -n "$flags" ] && echo "Cookie '$name': $flags"
done
How to report it (without being low-info spam)
Standalone headers are almost always info / low. For the report to have value:
- Don't send 10 separate reports. One report with a table of missing headers + aggregated impact.
- Demonstrate real impact. "CSP allows
unsafe-inline" is not actionable. "CSP allowsunsafe-inlineAND there's a reflected XSS in /search → direct bypass" → Medium. - Chain it with another bug. Missing header + working bug = severity boosted.
- Specific bug bounty programs. Some (Shopify, GitHub) explicitly say "Missing security headers = N/A". Read the scope.
Header report template
## Summary
The application lacks the following security headers, exposing users to clickjacking, amplified XSS and token leak via Referer.
## Affected headers
| Header | Expected | Actual |
|---|---|---|
| CSP | `default-src 'self'; frame-ancestors 'none'` | Absent |
| HSTS | `max-age=31536000; includeSubDomains` | `max-age=300` |
| X-Frame-Options | `DENY` | Absent |
## Impact
- No CSP: any reflected/stored XSS executes without restriction.
- Weak HSTS: 5-minute window for a MITM downgrade.
- No X-Frame-Options: clickjacking on the /transfer endpoint is possible.
## Clickjacking PoC
[HTML with an iframe of the /transfer endpoint + malicious overlay]
Hunting checklist
-
curl -sIthe target → list all response headers -
securityheaders.comfor a target grade (A+ is the goal) - CSP in
csp-evaluator.withgoogle.com→ look forunsafe-inline,unsafe-eval, whitelisted* - HSTS:
max-age >= 31536000+includeSubDomains+preload - Auth cookies:
Secure+HttpOnly+SameSite=Lax|Strict - Referrer-Policy present and restrictive
- X-Frame-Options or CSP
frame-ancestorson ALL pages - Permissions-Policy disabling unused APIs
- COOP
same-originat minimum on auth-critical pages - Cache-Control on authenticated endpoints:
no-store, private - Try a chain with XSS/CSRF/clickjacking to raise the severity
Related labs
Practice exploiting applications with misconfigured headers and XSS → CSP bypass chains in the Security Headers labs.
Practice this in a lab
Security Headers
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
Why `frame-ancestors 'none'` in the CSP is NOT enough to prevent clickjacking in 2026, and the CSP rule that actually closes every vector.
€7.99/mo · cancel anytime