Files
T

313 lines
11 KiB
Python

#!/usr/bin/env python3
"""Content Security Policy (CSP) analysis and bypass testing agent.
Fetches and analyzes CSP headers from web applications to identify
misconfigurations, overly permissive directives, and potential bypass
vectors. Tests for unsafe-inline, unsafe-eval, wildcard sources,
missing directives, and known CSP bypass patterns.
AUTHORIZED TESTING ONLY: Only use against targets you have explicit
written permission to test.
"""
import argparse
import json
import re
import sys
from datetime import datetime, timezone
try:
import requests
except ImportError:
print("[!] 'requests' library required: pip install requests", file=sys.stderr)
sys.exit(1)
CSP_DIRECTIVES = [
"default-src", "script-src", "style-src", "img-src", "font-src",
"connect-src", "media-src", "object-src", "frame-src", "child-src",
"worker-src", "frame-ancestors", "form-action", "base-uri",
"manifest-src", "prefetch-src", "navigate-to",
]
def fetch_csp(url, headers=None, cookies=None):
"""Fetch CSP header(s) from a URL."""
print(f"[*] Fetching CSP from {url}")
h = headers or {}
c = cookies or {}
resp = requests.get(url, headers=h, cookies=c, timeout=15, allow_redirects=True)
csp_header = resp.headers.get("Content-Security-Policy", "")
csp_ro = resp.headers.get("Content-Security-Policy-Report-Only", "")
print(f"[+] Status: {resp.status_code}")
if csp_header:
print(f"[+] CSP header found ({len(csp_header)} chars)")
else:
print("[!] No CSP header found")
if csp_ro:
print(f"[+] CSP-Report-Only header found ({len(csp_ro)} chars)")
return csp_header, csp_ro, resp.status_code
def parse_csp(csp_string):
"""Parse a CSP string into a structured dict."""
directives = {}
if not csp_string:
return directives
for part in csp_string.split(";"):
part = part.strip()
if not part:
continue
tokens = part.split()
if tokens:
directive_name = tokens[0].lower()
values = tokens[1:] if len(tokens) > 1 else []
directives[directive_name] = values
return directives
def analyze_csp(directives, csp_string):
"""Analyze CSP directives for security weaknesses."""
findings = []
# Missing CSP entirely
if not directives:
findings.append({
"check": "CSP Header Present",
"severity": "HIGH",
"status": "MISSING",
"description": "No Content-Security-Policy header",
"recommendation": "Implement a CSP header",
})
return findings
# Check for missing critical directives
if "default-src" not in directives:
findings.append({
"check": "default-src directive",
"severity": "HIGH",
"status": "MISSING",
"description": "No default-src fallback directive",
"recommendation": "Add default-src 'none' or default-src 'self'",
})
if "script-src" not in directives and "default-src" not in directives:
findings.append({
"check": "script-src directive",
"severity": "CRITICAL",
"status": "MISSING",
"description": "No script-src or default-src; scripts unrestricted",
})
if "object-src" not in directives:
findings.append({
"check": "object-src directive",
"severity": "MEDIUM",
"status": "MISSING",
"description": "Missing object-src; plugin-based XSS possible",
"recommendation": "Add object-src 'none'",
})
if "base-uri" not in directives:
findings.append({
"check": "base-uri directive",
"severity": "MEDIUM",
"status": "MISSING",
"description": "Missing base-uri; base tag injection possible",
"recommendation": "Add base-uri 'self' or base-uri 'none'",
})
if "frame-ancestors" not in directives:
findings.append({
"check": "frame-ancestors directive",
"severity": "MEDIUM",
"status": "MISSING",
"description": "Missing frame-ancestors; clickjacking possible",
"recommendation": "Add frame-ancestors 'self'",
})
# Analyze each directive
for directive, values in directives.items():
values_str = " ".join(values)
# unsafe-inline
if "'unsafe-inline'" in values:
sev = "CRITICAL" if directive in ("script-src", "default-src") else "MEDIUM"
findings.append({
"check": f"unsafe-inline in {directive}",
"severity": sev,
"status": "FAIL",
"description": f"'unsafe-inline' allows inline scripts/styles in {directive}",
"bypass": "Inject inline <script> or event handlers",
})
# unsafe-eval
if "'unsafe-eval'" in values:
findings.append({
"check": f"unsafe-eval in {directive}",
"severity": "CRITICAL" if directive in ("script-src", "default-src") else "HIGH",
"status": "FAIL",
"description": f"'unsafe-eval' allows eval() and similar in {directive}",
"bypass": "Use eval(), Function(), setTimeout('string') for XSS",
})
# Wildcard sources
if "*" in values:
findings.append({
"check": f"Wildcard (*) in {directive}",
"severity": "HIGH",
"status": "FAIL",
"description": f"Wildcard source in {directive} allows loading from any origin",
"bypass": "Host payload on any domain",
})
# data: URI
if "data:" in values and directive in ("script-src", "default-src", "object-src"):
findings.append({
"check": f"data: URI in {directive}",
"severity": "HIGH",
"status": "FAIL",
"description": f"data: URI in {directive} allows inline data execution",
"bypass": "Use data:text/html payload or data:application/javascript",
})
# blob: URI
if "blob:" in values and directive in ("script-src", "default-src", "worker-src"):
findings.append({
"check": f"blob: URI in {directive}",
"severity": "MEDIUM",
"status": "FAIL",
"description": f"blob: URI in {directive} may allow bypass via Blob URLs",
})
# Known bypass CDNs
bypass_cdns = ["cdn.jsdelivr.net", "cdnjs.cloudflare.com", "unpkg.com",
"ajax.googleapis.com", "raw.githubusercontent.com"]
for cdn in bypass_cdns:
if cdn in values_str:
findings.append({
"check": f"Bypassable CDN in {directive}",
"severity": "HIGH",
"status": "FAIL",
"description": f"{cdn} in {directive} can host arbitrary scripts",
"bypass": f"Upload/find malicious script on {cdn}",
})
# http: in HTTPS context
if any(v.startswith("http:") for v in values):
findings.append({
"check": f"HTTP source in {directive}",
"severity": "MEDIUM",
"status": "FAIL",
"description": f"HTTP source allows MitM injection in {directive}",
})
return findings
def format_summary(url, directives, findings, csp_string):
"""Print analysis summary."""
print(f"\n{'='*60}")
print(f" CSP Analysis Report")
print(f"{'='*60}")
print(f" URL : {url}")
print(f" Directives : {len(directives)}")
print(f" Findings : {len(findings)}")
severity_counts = {}
for f in findings:
sev = f.get("severity", "INFO")
severity_counts[sev] = severity_counts.get(sev, 0) + 1
print(f"\n By Severity:")
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
count = severity_counts.get(sev, 0)
if count > 0:
print(f" {sev:10s}: {count}")
if directives:
print(f"\n Parsed Directives:")
for d, v in directives.items():
print(f" {d:20s}: {' '.join(v)[:60]}")
if findings:
print(f"\n Security Issues:")
for f in findings:
if f["severity"] in ("CRITICAL", "HIGH"):
bypass = f" | Bypass: {f['bypass']}" if f.get("bypass") else ""
print(f" [{f['severity']:8s}] {f['check']}{bypass}")
return severity_counts
def main():
parser = argparse.ArgumentParser(
description="CSP analysis and bypass testing agent (authorized testing only)"
)
parser.add_argument("--url", required=True, help="Target URL to fetch CSP from")
parser.add_argument("--csp-string", help="Analyze a CSP string directly instead of fetching")
parser.add_argument("--header", nargs="+", help="Custom headers (key:value)")
parser.add_argument("--cookie", help="Cookie string")
parser.add_argument("--output", "-o", help="Output JSON report path")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
if args.csp_string:
csp_string = args.csp_string
csp_ro = ""
status_code = 0
else:
headers = {}
if args.header:
for h in args.header:
k, v = h.split(":", 1)
headers[k.strip()] = v.strip()
cookies = {}
if args.cookie:
for pair in args.cookie.split(";"):
if "=" in pair:
k, v = pair.strip().split("=", 1)
cookies[k] = v
csp_string, csp_ro, status_code = fetch_csp(args.url, headers or None, cookies or None)
directives = parse_csp(csp_string)
findings = analyze_csp(directives, csp_string)
if csp_ro:
ro_directives = parse_csp(csp_ro)
findings.append({
"check": "CSP-Report-Only mode",
"severity": "MEDIUM",
"status": "WARN",
"description": "CSP in report-only mode does not enforce restrictions",
})
severity_counts = format_summary(args.url, directives, findings, csp_string)
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tool": "CSP Analyzer",
"url": args.url,
"csp_header": csp_string,
"csp_report_only": csp_ro,
"directives": {k: v for k, v in directives.items()},
"findings": findings,
"severity_counts": severity_counts,
"risk_level": (
"CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
else "HIGH" if severity_counts.get("HIGH", 0) > 0
else "MEDIUM" if severity_counts.get("MEDIUM", 0) > 0
else "LOW"
),
}
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
print(f"\n[+] Report saved to {args.output}")
elif args.verbose:
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()