""" csp_scanner.py — Content Security Policy (CSP) Deep Auditor ============================================================ Parses and analyses the Content-Security-Policy (and CSP-Report-Only) header: - Detects missing CSP entirely - Flags unsafe-inline / unsafe-eval in script-src - Flags wildcard (*) sources - Detects missing critical directives - Identifies CSP bypass vectors (data:, blob:, http: schemes) - Checks for report-uri / report-to configuration - Scores overall CSP strength """ import re import urllib.request import urllib.error from scanners.base_scanner import BaseScanner CRITICAL_DIRECTIVES = [ "default-src", "script-src", "style-src", "img-src", "connect-src", "font-src", "object-src", "frame-ancestors", ] BYPASS_SCHEMES = ["data:", "blob:", "http:", "javascript:"] CDNS_ALLOWLISTED_BUT_RISKY = [ "cdn.jsdelivr.net", "unpkg.com", "cdnjs.cloudflare.com", "ajax.googleapis.com", "code.jquery.com", ] class CspScanner(BaseScanner): SCANNER_NAME = "Content Security Policy (CSP) Auditor" _SCANNER_KEY = "csp" def __init__(self, scan_id, target, domain, **kwargs): super().__init__(scan_id, target, domain, **kwargs) def run(self) -> list: self.log("INFO", f"[CSP] Auditing Content-Security-Policy on {self.target}...") try: headers = self._fetch_headers() csp = headers.get("content-security-policy", "") csp_ro = headers.get("content-security-policy-report-only", "") if not csp and not csp_ro: self.log("WARNING", "[CSP] No Content-Security-Policy header found!") self.add_vuln( title="Content Security Policy (CSP) Not Configured", severity="High", category="Content Security Policy", cvss_score=7.5, description=f"The target `{self.target}` does not serve a " "Content-Security-Policy header. Without CSP, the browser has " "no policy to enforce, making XSS attacks significantly more " "damaging as injected scripts run with full page privileges.", remediation="Implement a strict CSP:\n" "Content-Security-Policy: default-src 'self'; " "script-src 'self' 'nonce-{random}'; " "object-src 'none'; base-uri 'self'; frame-ancestors 'none'", evidence="No Content-Security-Policy header in response", request_details=f"GET {self.target}", response_details="Missing Content-Security-Policy header", confidence="Confirmed", ) return self.vulns active_csp = csp or csp_ro label = "CSP-Report-Only" if not csp else "CSP" self.log("INFO", f"[CSP] {label} header found. Parsing directives...") if not csp and csp_ro: self.add_vuln( title="CSP Deployed in Report-Only Mode (Not Enforced)", severity="Medium", category="Content Security Policy", cvss_score=5.3, description="A Content-Security-Policy-Report-Only header is present but " "no enforcing CSP header exists. Report-Only mode does not prevent " "attacks — it only sends violation reports.", remediation="Promote the policy to Content-Security-Policy once verified.", evidence="Content-Security-Policy-Report-Only present, no enforcing CSP", request_details=f"GET {self.target}", response_details="Report-Only mode CSP detected", confidence="High", ) directives = self._parse_csp(active_csp) self._audit_directives(directives, active_csp) except Exception as e: self.log("ERROR", f"[CSP] Audit error: {e}") self.log( "SUCCESS" if not self.vulns else "WARNING", f"[CSP] Audit complete. {len(self.vulns)} issue(s) found.", ) return self.vulns def _fetch_headers(self) -> dict: body, status, resp_headers = self._make_request( self.target, return_response_obj=True ) if resp_headers: return {k.lower(): v for k, v in resp_headers.items()} return {} @staticmethod def _parse_csp(csp: str) -> dict: directives = {} for part in csp.split(";"): part = part.strip() if not part: continue tokens = part.split() if tokens: directives[tokens[0].lower()] = tokens[1:] return directives def _audit_directives(self, directives: dict, raw_csp: str): script_src = directives.get("script-src", directives.get("default-src", [])) if "'unsafe-inline'" in script_src: self.add_vuln( title="CSP Allows 'unsafe-inline' in script-src", severity="High", category="Content Security Policy", cvss_score=7.4, description="The CSP `script-src` directive includes `'unsafe-inline'`, " "which allows inline `