""" 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", )