Skip to content
Threats Guide

csp_analyzer.py

Content-Security-Policy analyzer

  • prevention
  • python
  • 80 lines

Run security tools only on systems you own or have written permission to test.

Run it

From the repository root, after pip install -r tools/requirements.txt
cd 02-web-application-security/xss-attacks/prevention
python3 csp_analyzer.py --help

Source

View on GitHub
#!/usr/bin/env python3"""Content-Security-Policy analyzerLocation: 02-web-application-security/xss-attacks/prevention/csp_analyzer.pyReads a CSP (from --policy text or fetched from --url) and lists the weaknesses that let XSSthrough despite having a policy. A CSP is a second line of defence behind output encoding.Checks: missing script-src/default-src, 'unsafe-inline', 'unsafe-eval', wildcard orscheme-only sources (https:, data:), missing object-src and base-uri, missing frame-ancestors.Exit codes: 0 = no issues, 1 = issues found, 2 = input problem.""" import argparseimport sys import requests  def parse_csp(policy):    directives = {}    for part in policy.split(";"):        tokens = part.strip().split()        if tokens:            directives.setdefault(tokens[0].lower(), tokens[1:])    return directives  def analyse(policy):    d = parse_csp(policy)    issues = []    script = d.get("script-src", d.get("default-src"))    if script is None:        issues.append(("HIGH", "no script-src or default-src: scripts are not restricted"))    else:        has_nonce = any(t.startswith(("'nonce-", "'sha256-", "'sha384-", "'sha512-")) for t in script)        if "'unsafe-inline'" in script and not (has_nonce and "'strict-dynamic'" in script or has_nonce):            issues.append(("HIGH", "script-src allows 'unsafe-inline' without a nonce or hash: injected scripts will run"))        if "'unsafe-eval'" in script:            issues.append(("MEDIUM", "script-src allows 'unsafe-eval' (eval and string timers)"))        for token in script:            if token == "*" or token in ("https:", "http:", "data:"):                issues.append(("HIGH", f"script-src source '{token}' is too broad: any matching host can supply scripts"))            elif token.startswith("*."):                issues.append(("MEDIUM", f"script-src wildcard '{token}' trusts every subdomain"))    if "object-src" not in d and not (d.get("default-src") and "'none'" in d["default-src"]):        issues.append(("MEDIUM", "no object-src 'none' (plugins can be abused)"))    if "base-uri" not in d:        issues.append(("MEDIUM", "no base-uri (an injected <base> tag can redirect relative script URLs)"))    if "frame-ancestors" not in d:        issues.append(("LOW", "no frame-ancestors (clickjacking protection)"))    return issues  def main():    parser = argparse.ArgumentParser(description="Content-Security-Policy analyzer")    group = parser.add_mutually_exclusive_group(required=True)    group.add_argument("--policy", help="CSP header value"); group.add_argument("--url", help="fetch the header from this URL")    args = parser.parse_args()    policy = args.policy    if args.url:        try:            policy = requests.get(args.url, timeout=10).headers.get("Content-Security-Policy")        except requests.exceptions.RequestException as exc:            print(f"[x] Could not fetch {args.url}: {exc}")            return 2        if not policy:            print("[x] The response has no Content-Security-Policy header")            return 1    issues = analyse(policy)    for severity, message in issues:        print(f"  [{severity}] {message}")    print("[+] No issues found" if not issues else f"[!] {len(issues)} issue(s)")    return 1 if issues else 0  if __name__ == "__main__":    sys.exit(main())