SentinelScan-WSS / backend /scanners /cors_scanner.py
larxius's picture
Deploy SentinelScan WSS to HF Spaces
d543fc1 verified
Raw
History Blame Contribute Delete
16.9 kB
"""
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}")