SentinelScan-WSS / backend /scanners /headers_scanner.py
larxius's picture
Deploy SentinelScan WSS to HF Spaces
d543fc1 verified
Raw
History Blame Contribute Delete
12.5 kB
"""
headers_scanner.py
------------------
Checks HTTP response headers for the presence / correct configuration of
security-critical headers. Performs a real HTTP request against the target.
"""
from scanners.base_scanner import BaseScanner
from utils.fingerprint_db import match_tech, find_cves
HEADERS_POLICY = {
"Strict-Transport-Security": (
"Medium", 6.1,
"HSTS forces browsers to use HTTPS exclusively, preventing SSL-stripping attacks.",
"Add to your web server config:\n"
" Nginx: add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains; preload\" always;\n"
" Apache: Header always set Strict-Transport-Security \"max-age=31536000; includeSubDomains\"",
),
"Content-Security-Policy": (
"High", 7.2,
"CSP restricts which resources the browser may load, mitigating XSS and data-injection attacks.",
"Define a strict policy:\n"
" add_header Content-Security-Policy \"default-src 'self'; script-src 'self'; "
"object-src 'none'; base-uri 'self';\" always;",
),
"X-Frame-Options": (
"Medium", 5.4,
"X-Frame-Options prevents clickjacking by restricting iframe embedding.",
"add_header X-Frame-Options \"DENY\" always;\n"
"(or SAMEORIGIN if you require same-origin iframes)",
),
"X-Content-Type-Options": (
"Low", 3.7,
"Prevents MIME-sniffing attacks by instructing browsers not to guess content-type.",
"add_header X-Content-Type-Options \"nosniff\" always;",
),
"Referrer-Policy": (
"Low", 3.1,
"Controls how much referrer information is sent with outgoing requests.",
"add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;",
),
"Permissions-Policy": (
"Low", 3.1,
"Permissions-Policy restricts access to browser APIs (camera, microphone, geolocation).",
"add_header Permissions-Policy \"geolocation=(), microphone=(), camera=()\" always;",
),
"X-XSS-Protection": (
"Low", 3.1,
"Legacy XSS auditor header (deprecated but still expected by some scanners).",
"add_header X-XSS-Protection \"1; mode=block\" always;",
),
"Cross-Origin-Opener-Policy": (
"Medium", 5.3,
"COOP isolates your document from other cross-origin documents, preventing XS-Leaks attacks.",
"add_header Cross-Origin-Opener-Policy \"same-origin\" always;\n"
" Options: same-origin, same-origin-allow-popups, unsafe-none",
),
"Cross-Origin-Embedder-Policy": (
"Medium", 5.3,
"COEP requires documents to be COOP-isolated, enabling powerful features like SharedArrayBuffer.",
"add_header Cross-Origin-Embedder-Policy \"require-corp\" always;\n"
" Options: require-corp, credentialless, unsafe-none",
),
"Cross-Origin-Resource-Policy": (
"Medium", 5.3,
"CORP protects your resources from being loaded by other origins, preventing Spectre-style attacks.",
"add_header Cross-Origin-Resource-Policy \"same-origin\" always;\n"
" Options: same-origin, same-site, cross-origin",
),
"Expect-CT": (
"Medium", 5.5,
"Expect-CT allows sites to report Certificate Transparency violations and enforce CT compliance.",
"add_header Expect-CT \"max-age=86400, enforce, report-uri=\"https://your-report-uri\" always;\n"
" Note: Being deprecated in favor of Certificate Transparency enforcement",
),
}
class HeadersScanner(BaseScanner):
SCANNER_NAME = "HTTP Security Headers Scanner"
_SCANNER_KEY = "headers"
def run(self) -> list[dict]:
self.log("INFO", "[Headers] Starting HTTP Security Headers audit...")
headers_found = {}
body, status, resp_headers = self._make_request(
self.target,
headers={"User-Agent": "LarShield/2.0 (Security Audit Bot)"},
timeout=10,
return_response_obj=True,
)
if status == 0:
self.log("WARNING", "[Headers] Could not reach target. Header audit skipped.")
return self.vulns
headers_found = {k.lower(): v for k, v in resp_headers.items()}
self.log("SUCCESS", f"[Headers] Connected to target. HTTP {status}. Analysing response headers...")
for header, (severity, cvss, description, remediation) in HEADERS_POLICY.items():
if header.lower() in headers_found:
val = headers_found[header.lower()]
self.log("SUCCESS", f"[Headers] \u2714 {header}: {val[:80]}")
else:
self.log("WARNING", f"[Headers] \u2718 Missing: {header} \u2014 {description[:80]}")
self.add_vuln(
title=f"Missing Security Header: {header}",
severity=severity,
category="Security Headers",
cvss_score=cvss,
description=(
f"The HTTP response from {self.target} does not include the '{header}' header. "
f"{description}"
),
remediation=remediation,
confidence="High",
)
leak_headers = ["server", "x-powered-by", "x-aspnet-version", "x-aspnetmvc-version", "x-php-version", "x-runtime", "x-nginx-version"]
for lh in leak_headers:
if lh in headers_found:
self.log("WARNING", f"[Headers] Information Disclosure: '{lh}': {headers_found[lh]}")
self.add_vuln(
title=f"Server Information Disclosure via '{lh}' Header",
severity="Low",
category="Security Headers",
cvss_score=3.1,
description=(
f"The response header '{lh}' reveals server technology details: "
f"'{headers_found[lh]}'. This aids fingerprinting and targeted attacks."
),
remediation=(
f"Remove or obfuscate the '{lh}' header in your server configuration.\n"
" Nginx: server_tokens off;\n"
" Apache: ServerTokens Prod; ServerSignature Off"
),
confidence="Confirmed",
)
self._check_cookie_security(headers_found)
self._test_cache_poisoning()
fp_matches = match_tech(body if body else "", headers_found)
for fp in fp_matches:
cves = find_cves(fp["name"], fp.get("version", ""))
if cves:
cve_ids = [c["cve"] for c in cves]
self.log("WARNING", f"[Headers] Known CVEs for {fp['name']}: {', '.join(cve_ids)}")
self.add_vuln(
title=f"Known CVEs for Server Technology: {fp['name']}",
severity="High", category="Security Headers", cvss_score=max(c["cvss"] for c in cves),
description=f"Server technology {fp['name']} has known CVEs: {', '.join(cve_ids)}.",
remediation=f"Upgrade {fp['name']} to the latest version.",
evidence=f"CVEs: {', '.join(cve_ids)}",
confidence="Confirmed",
cve_ids=cve_ids,
)
self.log("INFO", f"[Headers] Header audit complete. {len(self.vulns)} issue(s) found.")
return self.vulns
def _check_cookie_security(self, headers_found):
if "set-cookie" not in headers_found:
return
cookies = headers_found["set-cookie"]
if isinstance(cookies, str):
cookies = [cookies]
for cookie in cookies:
cookie_lower = cookie.lower()
if "secure" not in cookie_lower:
self.log("WARNING", f"[Headers] Cookie missing Secure attribute: {cookie[:50]}")
self.add_vuln(
title="Cookie Missing Secure Attribute",
severity="Medium",
category="Security Headers",
cvss_score=5.5,
description=f"Cookie '{cookie[:50]}...' is missing the Secure attribute, allowing transmission over HTTP.",
remediation="Add the 'Secure' attribute to all cookies to ensure they are only sent over HTTPS.",
confidence="Confirmed",
)
if "httponly" not in cookie_lower:
self.log("WARNING", f"[Headers] Cookie missing HttpOnly attribute: {cookie[:50]}")
self.add_vuln(
title="Cookie Missing HttpOnly Attribute",
severity="Medium",
category="Security Headers",
cvss_score=5.5,
description=f"Cookie '{cookie[:50]}...' is missing the HttpOnly attribute, making it accessible to JavaScript.",
remediation="Add the 'HttpOnly' attribute to cookies to prevent XSS from accessing them.",
confidence="Confirmed",
)
if "samesite" not in cookie_lower:
self.log("WARNING", f"[Headers] Cookie missing SameSite attribute: {cookie[:50]}")
self.add_vuln(
title="Cookie Missing SameSite Attribute",
severity="Medium",
category="Security Headers",
cvss_score=5.3,
description=f"Cookie '{cookie[:50]}...' is missing the SameSite attribute, vulnerable to CSRF attacks.",
remediation="Add the 'SameSite=Strict' or 'SameSite=Lax' attribute to cookies to prevent CSRF.",
confidence="Confirmed",
)
def _test_cache_poisoning(self):
poison_header = "poison.attacker.com"
body, status, _ = self._make_request(
self.target,
headers={
"User-Agent": "LarShield/2.0",
"X-Forwarded-Host": poison_header,
"X-Host": poison_header,
},
timeout=5,
return_response_obj=True,
)
if body and poison_header in body:
self.log("CRITICAL", "[Headers] CACHE POISONING VULNERABILITY DETECTED via X-Forwarded-Host!")
self.add_vuln(
title="Web Cache Poisoning via X-Forwarded-Host",
severity="High",
category="Configuration",
cvss_score=8.6,
description=f"The server reflects the injected unkeyed header 'X-Forwarded-Host: {poison_header}' in the response body. If this response is cached by a CDN or reverse proxy, the malicious payload will be served to all subsequent users visiting the page.",
remediation="Ensure that headers like 'X-Forwarded-Host' and 'X-Host' are either ignored or explicitly included in the CDN/proxy cache key. Validate the Host header against a strict allowlist.",
confidence="Confirmed",
evidence=f"Reflected payload: {poison_header}",
payload=f"X-Forwarded-Host: {poison_header}",
)
body, status, _ = self._make_request(
self.target,
headers={
"User-Agent": "LarShield/2.0",
"X-Forwarded-Proto": "http",
},
timeout=5,
return_response_obj=True,
)
if body and ("http://" in body or "evil.com" in body):
self.log("CRITICAL", "[Headers] CACHE POISONING VULNERABILITY DETECTED via X-Forwarded-Proto!")
self.add_vuln(
title="Web Cache Poisoning via X-Forwarded-Proto",
severity="High",
category="Configuration",
cvss_score=8.6,
description="The server reflects X-Forwarded-Proto header in responses, potentially allowing protocol downgrade attacks through cache poisoning.",
remediation="Ignore or properly validate X-Forwarded-Proto header. Ensure HTTPS is enforced regardless of proxy headers.",
confidence="Confirmed",
payload="X-Forwarded-Proto: http",
)