mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 06:04:56 +03:00
313 lines
11 KiB
Python
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()
|