""" host_header_scanner.py — Host Header Injection Scanner ======================================================= Tests for Host and X-Forwarded-Host header injection, checking if the injected arbitrary domain is reflected in redirects (Location) or internal link generation (password reset poisoning). """ import urllib.parse, time from scanners.base_scanner import BaseScanner from utils.anomaly import TimingAnomalyDetector from utils.callback import build_callback_url class HostHeaderScanner(BaseScanner): SCANNER_NAME = "Host Header Injection Scanner" _SCANNER_KEY = "host_header" def __init__(self, scan_id, target, domain, **kwargs): super().__init__(scan_id, target, domain, **kwargs) self._timing = TimingAnomalyDetector() def run(self) -> list: self.log("INFO", f"[HostHeader] Testing Host header injection on {self.target}...") self._cwe = ["CWE-644"] self._owasp = "A04:2021 – Insecure Design" self._test_host_override() self._test_x_forwarded_host() self._test_x_host() self._test_x_original_host() self._test_port_confusion() self._test_absolute_url_in_host() self._test_callback_injection() self._test_timing_anomaly() if not self.vulns: self.log("SUCCESS", "[HostHeader] No Host header reflection detected.") return self.vulns def _test_host_override(self): payload = "evil-host-injection.com" body, status, resp_headers = self._make_request( self.target, headers={"User-Agent": "LarShield/2.0", "Host": payload}, return_response_obj=True, ) if body is None: self.log("ERROR", "[HostHeader] Host override request failed") return self._check_reflection( resp_headers.get("Location", "") if resp_headers else "", body, payload, "Host" ) def _test_x_forwarded_host(self): payload = "evil-xfh-injection.com" body, status, resp_headers = self._make_request( self.target, headers={"User-Agent": "LarShield/2.0", "X-Forwarded-Host": payload}, return_response_obj=True, ) if body is None: self.log("ERROR", "[HostHeader] X-Forwarded-Host request failed") return self._check_reflection( resp_headers.get("Location", "") if resp_headers else "", body, payload, "X-Forwarded-Host" ) def _test_x_host(self): payload = "evil-xhost-injection.com" body, status, resp_headers = self._make_request( self.target, headers={"X-Host": payload}, return_response_obj=True, ) if body and resp_headers: self._check_reflection( resp_headers.get("Location", "") if resp_headers else "", body, payload, "X-Host" ) def _test_x_original_host(self): payload = "evil-xoriginal-injection.com" body, status, resp_headers = self._make_request( self.target, headers={"X-Original-Host": payload}, return_response_obj=True, ) if body and resp_headers: self._check_reflection( resp_headers.get("Location", "") if resp_headers else "", body, payload, "X-Original-Host" ) def _test_port_confusion(self): payload = "localhost:8080" body, status, resp_headers = self._make_request( self.target, headers={"User-Agent": "LarShield/2.0", "Host": payload}, return_response_obj=True, ) if body is None: self.log("ERROR", "[HostHeader] Port confusion request failed") return if resp_headers: location = resp_headers.get("Location", "") if payload in location or "localhost" in location: self.add_vuln( title="Host Header Injection — Port Confusion", severity="Medium", category="Host Header Injection", cvss_score=6.1, description=f"Setting Host header to '{payload}' caused the server to include " "this host:port in the Location header. This can be used for open redirect " "or SSRF attacks against internal services.", remediation="Validate the Host header against an allowlist of valid domains and ports.", evidence=f"Location header: {location}", payload=f"Host: {payload}", request_details=f"GET with Host: {payload}", response_details=f"Location: {location}", confidence="High", cwe_ids=self._cwe, owasp_category=self._owasp, ) self.log("WARNING", f"[HostHeader] Port confusion via Host: {payload}") def _test_absolute_url_in_host(self): payload = "https://evil-absolute.com" body, status, resp_headers = self._make_request( self.target, headers={"User-Agent": "LarShield/2.0", "Host": payload}, return_response_obj=True, ) if body is None: self.log("ERROR", "[HostHeader] Absolute URL in Host test failed") return if resp_headers: location = resp_headers.get("Location", "") if payload in location or payload in body: ref = "Location header" if payload in location else "response body" self.add_vuln( title="Host Header Injection — Absolute URL in Host", severity="High", category="Host Header Injection", cvss_score=7.4, description=f"Injecting an absolute URL '{payload}' in the Host header was " f"reflected in the {ref}. This enables open redirect and phishing attacks.", remediation="Reject non-hostname values (absolute URLs) in the Host header. " "Only allow valid domain names.", evidence=f"'{payload}' reflected in {ref}", payload=f"Host: {payload}", request_details=f"GET with Host: {payload}", response_details=f"Reflected in {ref}", confidence="Confirmed", cwe_ids=self._cwe, owasp_category=self._owasp, ) self.log("WARNING", f"[HostHeader] Absolute URL reflected in {ref}") def _test_callback_injection(self): callback_url = build_callback_url("/host-header") for hdr in ["Host", "X-Forwarded-Host", "X-Host", "X-Original-Host"]: body, status, resp_headers = self._make_request( self.target, headers={hdr: callback_url}, return_response_obj=True, ) if body and callback_url in body: self.add_vuln( title=f"Blind Host Header Injection via Callback URL in {hdr}", severity="High", category="Host Header Injection", cvss_score=8.2, description=f"Injecting a callback URL in the `{hdr}` header reflects the value " "in the server response. This enables blind host header injection with out-of-band " "detection and can be used for password reset poisoning or SSRF.", remediation="Validate and allowlist the Host header. " "Do not reflect unvalidated header values in responses.", evidence=f"Callback URL '{callback_url}' reflected in response body", payload=f"{hdr}: {callback_url}", request_details=f"GET with {hdr}: {callback_url}", response_details="Callback URL reflected in body", confidence="Confirmed", cwe_ids=self._cwe, owasp_category=self._owasp, ) self.log("WARNING", f"[HostHeader] Callback injection via {hdr}!") def _test_timing_anomaly(self): self.log("INFO", "[HostHeader] Testing timing anomalies for host overrides...") for hdr in ["Host", "X-Forwarded-Host", "X-Host", "X-Original-Host"]: for _ in range(3): body, status, elapsed = self._make_timed_request( self.target, headers={hdr: "timing-test.example.com"}, ) self._timing.record_timing(f"baseline_{hdr}", elapsed) t0 = time.monotonic() body, status = self._make_request( self.target, headers={hdr: "evil-timing-test.example.com"}, ) elapsed = time.monotonic() - t0 if self._timing.test_payload(f"anomaly_{hdr}", elapsed, "evil-timing-test.example.com", 3.0): self.log("WARNING", f"[HostHeader] Timing anomaly detected for {hdr} override") def _check_reflection(self, location: str, body: str, payload: str, header_name: str): cwe = self._cwe owasp = self._owasp if payload in location: self.add_vuln( title=f"Host Header Injection (Redirect) via {header_name}", severity="Medium", category="Host Header Injection", cvss_score=6.1, description=f"Injecting `{header_name}: {payload}` causes the server to redirect " f"users to the injected domain (Location: {location}). This can be used for phishing " f"and open redirects.", remediation="Ensure the application uses a hardcoded domain or validates the Host header " "against an allowlist. Do not trust the Host or X-Forwarded-Host headers for redirects.", evidence=f"Location header: {location}", payload=f"{header_name}: {payload}", request_details=f"GET with {header_name}: {payload}", response_details=f"Location: {location}", confidence="Confirmed", cwe_ids=cwe, owasp_category=owasp, ) self.log("WARNING", f"[HostHeader] Redirect reflection found via {header_name}") elif f'"{payload}' in body or f"'{payload}" in body or f"//{payload}" in body: self.add_vuln( title=f"Host Header Reflection (Body) via {header_name}", severity="High", category="Host Header Injection", cvss_score=7.4, description=f"Injecting `{header_name}: {payload}` reflects the attacker-controlled " f"domain in the HTML body (e.g., in links, script tags, or meta tags). This often " f"leads to Password Reset Poisoning or Web Cache Poisoning.", remediation="Configure the web application to use a statically defined site URL for link " "generation instead of dynamically extracting it from the request headers.", evidence=f"Payload '{payload}' found in response body", payload=f"{header_name}: {payload}", request_details=f"GET with {header_name}: {payload}", response_details="Payload reflected in HTML body", confidence="Confirmed", cwe_ids=cwe, owasp_category=owasp, ) self.log("WARNING", f"[HostHeader] Body reflection found via {header_name}") else: self.log("SUCCESS", f"[HostHeader] {header_name}: No reflection detected")