#!/usr/bin/env python3 """Agent for testing CORS misconfiguration vulnerabilities during authorized assessments.""" import os import requests import json import argparse import urllib3 from datetime import datetime from urllib.parse import urlparse urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) EVIL_ORIGINS = [ "https://evil.com", "null", "http://localhost", "http://localhost:3000", "http://127.0.0.1", ] def build_dynamic_origins(target_domain): """Generate domain-specific bypass origins for testing.""" return [ f"https://evil.{target_domain}", f"https://{target_domain}.evil.com", f"https://evil{target_domain}", f"http://{target_domain}", f"https://sub.{target_domain}", ] def test_origin_reflection(url, origins, cookies=None): """Test if server reflects arbitrary Origin headers.""" print(f"\n[*] Testing origin reflection on {url}") findings = [] for origin in origins: try: headers = {"Origin": origin} resp = requests.get(url, headers=headers, cookies=cookies, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments acao = resp.headers.get("Access-Control-Allow-Origin", "") acac = resp.headers.get("Access-Control-Allow-Credentials", "") if acao and acao != "": reflected = acao == origin creds = acac.lower() == "true" severity = "CRITICAL" if reflected and creds else ( "HIGH" if reflected else "INFO") if reflected: findings.append({ "url": url, "origin": origin, "acao": acao, "credentials": creds, "severity": severity, }) cred_str = " + credentials" if creds else "" print(f" [{'!' if severity != 'INFO' else '+'}] Origin '{origin}' -> " f"ACAO: {acao}{cred_str} [{severity}]") except requests.RequestException: continue return findings def test_preflight(url, origin="https://evil.com"): """Test OPTIONS preflight request handling.""" print(f"\n[*] Testing preflight (OPTIONS) on {url}") findings = [] methods_to_test = ["GET", "POST", "PUT", "DELETE", "PATCH"] for method in methods_to_test: try: headers = { "Origin": origin, "Access-Control-Request-Method": method, "Access-Control-Request-Headers": "Authorization, Content-Type", } resp = requests.options(url, headers=headers, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments acam = resp.headers.get("Access-Control-Allow-Methods", "") acah = resp.headers.get("Access-Control-Allow-Headers", "") max_age = resp.headers.get("Access-Control-Max-Age", "") if method in acam: print(f" [+] {method} allowed in preflight") if max_age and int(max_age) > 86400: findings.append({ "url": url, "issue": "excessive_max_age", "max_age": max_age, "severity": "MEDIUM", }) print(f" [!] Max-Age too long: {max_age}s (>86400)") except requests.RequestException: continue return findings def test_wildcard_with_credentials(url): """Test for wildcard CORS with credentials (invalid but sometimes misconfigured).""" print(f"\n[*] Testing wildcard + credentials on {url}") try: resp = requests.get(url, headers={"Origin": "https://any.com"}, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments acao = resp.headers.get("Access-Control-Allow-Origin", "") acac = resp.headers.get("Access-Control-Allow-Credentials", "") if acao == "*" and acac.lower() == "true": print(f" [!] CRITICAL: Wildcard (*) with credentials=true") return [{"url": url, "issue": "wildcard_with_credentials", "severity": "CRITICAL"}] elif acao == "*": print(f" [+] Wildcard (*) without credentials (acceptable for public APIs)") except requests.RequestException: pass return [] def test_null_origin(url, cookies=None): """Test if null Origin is accepted (exploitable via sandboxed iframes).""" print(f"\n[*] Testing null origin on {url}") try: resp = requests.get(url, headers={"Origin": "null"}, cookies=cookies, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments acao = resp.headers.get("Access-Control-Allow-Origin", "") acac = resp.headers.get("Access-Control-Allow-Credentials", "") if acao == "null": creds = acac.lower() == "true" severity = "HIGH" if creds else "MEDIUM" print(f" [!] Null origin accepted (credentials: {creds}) [{severity}]") return [{"url": url, "issue": "null_origin_accepted", "credentials": creds, "severity": severity}] else: print(f" [+] Null origin not reflected") except requests.RequestException: pass return [] def test_internal_origins(url, cookies=None): """Test if internal/development origins are trusted.""" print(f"\n[*] Testing internal origins on {url}") internal = [ "http://localhost", "http://localhost:3000", "http://localhost:8080", "http://127.0.0.1", "http://10.0.0.1", "http://192.168.1.1", ] findings = [] for origin in internal: try: resp = requests.get(url, headers={"Origin": origin}, cookies=cookies, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true") # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments acao = resp.headers.get("Access-Control-Allow-Origin", "") if acao == origin: findings.append({"url": url, "origin": origin, "severity": "MEDIUM"}) print(f" [!] Internal origin accepted: {origin}") except requests.RequestException: continue return findings def scan_endpoints(base_url, endpoints, token=None): """Scan multiple endpoints for CORS issues.""" all_findings = [] cookies = None headers_auth = {"Authorization": f"Bearer {token}"} if token else {} domain = urlparse(base_url).netloc dynamic_origins = build_dynamic_origins(domain) test_origins = EVIL_ORIGINS + dynamic_origins for endpoint in endpoints: url = f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}" all_findings.extend(test_origin_reflection(url, test_origins)) all_findings.extend(test_null_origin(url)) all_findings.extend(test_wildcard_with_credentials(url)) all_findings.extend(test_preflight(url)) all_findings.extend(test_internal_origins(url)) return all_findings def generate_report(findings, output_path): """Generate CORS misconfiguration assessment report.""" report = { "assessment_date": datetime.now().isoformat(), "total_findings": len(findings), "by_severity": {}, "findings": findings, } for f in findings: sev = f.get("severity", "INFO") report["by_severity"][sev] = report["by_severity"].get(sev, 0) + 1 with open(output_path, "w") as fh: json.dump(report, fh, indent=2) print(f"\n[*] Report saved to {output_path}") for sev, count in report["by_severity"].items(): print(f" {sev}: {count}") def main(): parser = argparse.ArgumentParser(description="CORS Misconfiguration Testing Agent") parser.add_argument("base_url", help="Base URL of the target") parser.add_argument("--endpoints", nargs="+", default=["/api/user/profile", "/api/users", "/api/account"]) parser.add_argument("--token", help="Bearer token for authenticated testing") parser.add_argument("-o", "--output", default="cors_report.json") args = parser.parse_args() print(f"[*] CORS Misconfiguration Assessment") print(f"[*] Target: {args.base_url}") findings = scan_endpoints(args.base_url, args.endpoints, args.token) generate_report(findings, args.output) if __name__ == "__main__": main()