Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """ | |
| cors_scanner.py — Tests for CORS (Cross-Origin Resource Sharing) misconfigurations. | |
| No external dependencies required. | |
| """ | |
| import urllib.request | |
| import urllib.error | |
| import urllib.parse | |
| from scanners.base_scanner import BaseScanner | |
| from utils.callback import build_callback_url | |
| TEST_ORIGINS = [ | |
| "https://evil.attacker.com", | |
| "https://malicious-site.net", | |
| "null", | |
| ] | |
| CALLBACK_ORIGIN = build_callback_url("/cors").replace("https://", "https://callback.").replace("http://", "http://callback.") | |
| class CorsScanner(BaseScanner): | |
| SCANNER_NAME = "CORS Misconfiguration Scanner" | |
| _SCANNER_KEY = "cors" | |
| def _make_ctx(self): | |
| return self.get_ssl_context() | |
| def run(self): | |
| self.log("INFO", f"[CORS] Testing Cross-Origin Resource Sharing policy on {self.target}...") | |
| for origin in TEST_ORIGINS: | |
| self._test_origin(origin) | |
| self._test_origin(CALLBACK_ORIGIN) | |
| self._test_subdomain_trust() | |
| self._test_preflight() | |
| self._test_preflight_credentials() | |
| self.log("INFO", f"[CORS] CORS analysis complete. {len(self.vulns)} issue(s) found.") | |
| return self.vulns | |
| def _test_origin(self, origin): | |
| try: | |
| headers = { | |
| "User-Agent": "LarShield/2.0", | |
| "Origin": origin, | |
| } | |
| if self.auth_headers: | |
| headers.update(self.auth_headers) | |
| req = urllib.request.Request( | |
| self.target, | |
| headers=headers, | |
| ) | |
| with urllib.request.urlopen(req, timeout=8, context=self._make_ctx()) as resp: | |
| headers = {k.lower(): v for k, v in resp.getheaders()} | |
| acao = headers.get("access-control-allow-origin", "") | |
| acac = headers.get("access-control-allow-credentials", "").lower() | |
| if acao == "*": | |
| self.log("WARNING", f"[CORS] Wildcard ACAO header: Access-Control-Allow-Origin: *") | |
| if acac == "true": | |
| self.log("CRITICAL", "[CORS] CRITICAL: Wildcard CORS + credentials=true!") | |
| self.add_vuln( | |
| title="Critical CORS: Wildcard Origin with Credentials Allowed", | |
| severity="Critical", category="CORS", cvss_score=9.8, | |
| description="The server responds with Access-Control-Allow-Origin: * AND Access-Control-Allow-Credentials: true. This combination is exploitable — any malicious website can make authenticated cross-origin requests on behalf of logged-in users, enabling account takeover and data theft.", | |
| remediation="Never combine wildcard ACAO with credentials. Use explicit allowlisted origins:\n Access-Control-Allow-Origin: https://yourdomain.com\n Access-Control-Allow-Credentials: true", | |
| evidence=f"Response headers: Access-Control-Allow-Origin: *, Access-Control-Allow-Credentials: true", | |
| payload=f"Origin: {origin}", | |
| request_details="GET with Origin header", | |
| response_details=f"ACAO: *, ACAC: true", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| else: | |
| self.add_vuln( | |
| title="Overly Permissive CORS Policy (Wildcard Origin)", | |
| severity="Medium", category="CORS", cvss_score=5.4, | |
| description="The server allows cross-origin requests from any domain (Access-Control-Allow-Origin: *). This may expose public APIs to abuse from malicious websites.", | |
| remediation="Restrict CORS to specific trusted origins:\n Access-Control-Allow-Origin: https://yourfrontenddomain.com\nUse environment-based origin allowlists.", | |
| evidence=f"Response header: Access-Control-Allow-Origin: *", | |
| payload=f"Origin: {origin}", | |
| request_details="GET with Origin header", | |
| response_details=f"ACAO: *", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| elif acao == origin and origin != "null": | |
| self.log("WARNING", f"[CORS] Origin reflected back: {origin} -> ACAO: {acao}") | |
| if acac == "true": | |
| self.log("CRITICAL", f"[CORS] Arbitrary origin reflected with credentials=true!") | |
| self.add_vuln( | |
| title="CORS: Arbitrary Origin Reflected with Credentials", | |
| severity="Critical", category="CORS", cvss_score=9.1, | |
| description=f"The server reflected the attacker-controlled origin '{origin}' in the ACAO header AND allows credentials. This enables cross-origin authenticated requests from any malicious website — a classic CORS-based account takeover vector.", | |
| remediation="Implement an explicit origin allowlist. Never reflect arbitrary origins:\n allowed = ['https://app.yourdomain.com']\n if request.headers.get('Origin') in allowed:\n response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']", | |
| evidence=f"Origin: {origin} -> ACAO: {acao}, ACAC: {acac}", | |
| payload=f"Origin: {origin}", | |
| request_details="GET with Origin header", | |
| response_details=f"ACAO: {acao}, ACAC: {acac}", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| else: | |
| self.add_vuln( | |
| title="CORS: Arbitrary Origin Reflected Without Credentials", | |
| severity="Medium", category="CORS", cvss_score=5.4, | |
| description=f"The server reflected the arbitrary origin '{origin}'. While credentials are not allowed, this still permits cross-origin data theft of non-credentialed responses.", | |
| remediation="Use a static allowlist of trusted origins rather than reflecting the request Origin header.", | |
| evidence=f"Origin: {origin} -> ACAO: {acao}", | |
| payload=f"Origin: {origin}", | |
| request_details="GET with Origin header", | |
| response_details=f"ACAO: {acao}", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| elif origin == "null" and acao == "null": | |
| self.log("WARNING", "[CORS] null origin accepted! Sandboxed iframes can exploit this.") | |
| self.add_vuln( | |
| title="CORS: Null Origin Accepted", | |
| severity="Medium", category="CORS", cvss_score=6.1, | |
| description="The server accepts 'null' as a valid CORS origin. Sandboxed iframes or locally opened HTML files send Origin: null, which attackers can exploit to bypass CORS restrictions.", | |
| remediation="Remove 'null' from any CORS origin allowlist. Only accept explicit https:// origins.", | |
| evidence="Origin: null -> ACAO: null", | |
| payload="Origin: null", | |
| request_details="GET with Origin: null", | |
| response_details="ACAO: null", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| else: | |
| self.log("SUCCESS", f"[CORS] Origin '{origin}': Correctly rejected") | |
| except urllib.error.HTTPError as e: | |
| acao = e.headers.get("Access-Control-Allow-Origin", "") | |
| if acao: | |
| self.log("INFO", f"[CORS] HTTP {e.code} but ACAO header present: {acao}") | |
| except Exception as e: | |
| self.log("ERROR", f"[CORS] Request to {self.target} with origin '{origin}': {e}") | |
| def _test_subdomain_trust(self): | |
| try: | |
| parsed = urllib.parse.urlparse(self.target) | |
| domain = parsed.netloc | |
| sub_origin = f"{parsed.scheme}://evil.{domain}" | |
| headers = { | |
| "User-Agent": "LarShield/2.0", | |
| "Origin": sub_origin, | |
| } | |
| if self.auth_headers: | |
| headers.update(self.auth_headers) | |
| req = urllib.request.Request( | |
| self.target, | |
| headers=headers, | |
| ) | |
| with urllib.request.urlopen(req, timeout=8, context=self._make_ctx()) as resp: | |
| headers = {k.lower(): v for k, v in resp.getheaders()} | |
| acao = headers.get("access-control-allow-origin", "") | |
| acac = headers.get("access-control-allow-credentials", "").lower() | |
| if acao == sub_origin: | |
| self.log("WARNING", f"[CORS] Subdomain trust detected: {sub_origin} is allowed!") | |
| self.add_vuln( | |
| title="CORS: Subdomain Trust - Evil Subdomain Allowed", | |
| severity="High", category="CORS", cvss_score=7.0, | |
| description=f"The server trusts subdomains of its own origin. If any subdomain is compromised (e.g., via XSS or takeover), an attacker can make authenticated CORS requests.", | |
| remediation="Explicitly list only the exact subdomains that need CORS access, or use a dedicated API domain.", | |
| evidence=f"Origin: {sub_origin} -> ACAO: {acao}", | |
| payload=f"Origin: {sub_origin}", | |
| request_details="GET with subdomain Origin header", | |
| response_details=f"ACAO: {acao}, ACAC: {acac}", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| else: | |
| self.log("SUCCESS", f"[CORS] Subdomain '{sub_origin}': Correctly rejected") | |
| except Exception as e: | |
| self.log("ERROR", f"[CORS] Subdomain trust test error: {e}") | |
| def _test_preflight(self): | |
| try: | |
| req = urllib.request.Request( | |
| self.target, | |
| method="OPTIONS", | |
| headers={ | |
| "User-Agent": "LarShield/2.0", | |
| "Origin": "https://evil.attacker.com", | |
| "Access-Control-Request-Method": "DELETE", | |
| "Access-Control-Request-Headers": "Authorization", | |
| }, | |
| ) | |
| with urllib.request.urlopen(req, timeout=8, context=self._make_ctx()) as resp: | |
| headers = {k.lower(): v for k, v in resp.getheaders()} | |
| acam = headers.get("access-control-allow-methods", "") | |
| acah = headers.get("access-control-allow-headers", "") | |
| acao = headers.get("access-control-allow-origin", "") | |
| if acao == "https://evil.attacker.com": | |
| self.log("WARNING", "[CORS] Preflight reflects arbitrary origin!") | |
| self.add_vuln( | |
| title="CORS Preflight Origin Reflection", | |
| severity="Medium", category="CORS", cvss_score=5.4, | |
| description="The OPTIONS preflight response reflects the Origin header in Access-Control-Allow-Origin. This indicates a permissive CORS policy that trusts arbitrary origins.", | |
| remediation="Do not reflect the Origin header in preflight responses. Use explicit allowlisted origins.", | |
| evidence=f"Origin: https://evil.attacker.com -> ACAO: {acao}", | |
| payload="Origin: https://evil.attacker.com", | |
| request_details="OPTIONS with Origin: https://evil.attacker.com", | |
| response_details=f"ACAO: {acao}, ACAM: {acam}", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| if "DELETE" in acam.upper() or "PUT" in acam.upper(): | |
| self.log("WARNING", f"[CORS] Preflight allows dangerous methods: {acam}") | |
| self.add_vuln( | |
| title="CORS Preflight Allows Dangerous HTTP Methods", | |
| severity="Medium", category="CORS", cvss_score=5.4, | |
| description=f"The CORS preflight response allows dangerous HTTP methods from cross-origin requests: {acam}. Combined with a weak origin policy, this enables destructive cross-origin operations.", | |
| remediation="Restrict ACAM to only the methods your API actually requires:\n Access-Control-Allow-Methods: GET, POST\nNever include DELETE or PUT unless explicitly required cross-origin.", | |
| evidence=f"ACAM: {acam}", | |
| payload="Access-Control-Request-Method: DELETE", | |
| request_details="OPTIONS with Access-Control-Request-Method: DELETE", | |
| response_details=f"ACAM: {acam}, ACAH: {acah}", | |
| confidence="High", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| else: | |
| self.log("SUCCESS", "[CORS] Preflight method policy: Appropriate") | |
| if acah and "Authorization" in acah: | |
| self.log("INFO", "[CORS] Preflight allows Authorization header") | |
| except Exception as e: | |
| self.log("ERROR", f"[CORS] Preflight check error: {e}") | |
| def _test_preflight_credentials(self): | |
| try: | |
| req = urllib.request.Request( | |
| self.target, | |
| method="OPTIONS", | |
| headers={ | |
| "User-Agent": "LarShield/2.0", | |
| "Origin": "https://evil.attacker.com", | |
| "Access-Control-Request-Method": "GET", | |
| "Access-Control-Request-Headers": "Authorization", | |
| }, | |
| ) | |
| with urllib.request.urlopen(req, timeout=8, context=self._make_ctx()) as resp: | |
| headers = {k.lower(): v for k, v in resp.getheaders()} | |
| acac = headers.get("access-control-allow-credentials", "").lower() | |
| acao = headers.get("access-control-allow-origin", "") | |
| if acac == "true" and acao == "https://evil.attacker.com": | |
| self.log("CRITICAL", "[CORS] Preflight allows credentials with arbitrary origin!") | |
| self.add_vuln( | |
| title="CORS Preflight Allows Credentials with Arbitrary Origin", | |
| severity="Critical", category="CORS", cvss_score=9.1, | |
| description="The OPTIONS preflight response allows credentials AND reflects arbitrary origin. This enables authenticated cross-origin requests from any attacker-controlled domain.", | |
| remediation="Only allow credentials with specific allowlisted origins. Never reflect arbitrary origins with credentials=true.", | |
| evidence=f"ACAO: {acao}, ACAC: {acac}", | |
| payload="Origin: https://evil.attacker.com", | |
| request_details="OPTIONS with credentials check", | |
| response_details=f"ACAO: {acao}, ACAC: {acac}", | |
| confidence="Confirmed", | |
| cwe_ids=["CWE-942"], | |
| owasp_category="A01:2021 – Broken Access Control", | |
| ) | |
| except Exception as e: | |
| self.log("ERROR", f"[CORS] Preflight credentials test error: {e}") | |