critical Threat analysis

Starlette CVE-2026-48710: BadHost Authentication Bypass

Starlette CVE-2026-48710 (BadHost) is a Host-header URL reconstruction flaw fixed in Starlette 1.0.1. New OSTIF, X41, Tenable, and BadHost scanner sources clarify that the highest-risk deployments are FastAPI/Starlette/LLM services whose middleware makes security decisions from request.url.path.

#starlette#fastapi#zero-day#security-bypass
On this page 0% read

    Executive Summary

    A critical authentication bypass vulnerability, designated CVE-2026-48710 and nicknamed “BadHost”, has been disclosed in the Starlette web framework. New primary sources now clarify the exploit condition: Starlette versions before 1.0.1 reconstruct request.url from the raw HTTP Host header, while routing still uses the real ASGI path. Middleware that authorizes based on request.url.path can therefore see an attacker-controlled path that differs from the route actually executed OSTIF Tenable.

    The highest-risk deployments are not every Starlette application equally. Prioritize FastAPI, Starlette, vLLM, LiteLLM, MCP gateways, OpenAI-compatible proxies, and internal AI/agent services that run directly on ASGI servers or use middleware to gate /admin, /v1/models, /mcp, /internal, /metrics, or tool-execution endpoints BadHost OSTIF. This article provides an impact determination and a Python audit script to identify vulnerable dependencies and Host-header exploit attempts.

    Key Facts

    cve: "CVE-2026-48710"
    alias: "BadHost"
    vendor: "Starlette"
    product: "Starlette (ASGI toolkit)"
    disclosed_date: "2026-05-26"
    vulnerability: "Authentication bypass via Host header URL reconstruction injection"
    cwe: ["CWE-346", "CWE-284"]
    affected_products: ["Starlette", "FastAPI applications with affected middleware", "vLLM/LiteLLM/MCP services using affected middleware"]
    affected_versions: ["Starlette < 1.0.1"]
    fixed_versions: ["1.0.1"]
    nvd_cvss_v31: "6.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N"
    public_tools: ["badhost.org scanner", "X41 PoC / Semgrep / CodeQL rules"]
    exploitation_status: "public PoC and scanner available; active exploitation not confirmed in reviewed primary sources"
    kev_status: "not listed in CISA KEV as of 2026-05-27 review"

    Source Confidence & Evidence Mapping

    • confirmed: OSTIF says the issue was discovered during a vLLM audit managed by OSTIF and sponsored by Alpha-Omega, and describes the path mismatch between router dispatch and middleware-visible request.url.path OSTIF.
    • confirmed: Tenable’s CVE entry states that Starlette before 1.0.1 did not validate the HTTP Host request header before reconstructing request.url; it lists the 1.0.1 fix behavior and references the GHSA, X41 advisory, Starlette commit, and BadHost scanner Tenable.
    • confirmed: BadHost.org provides a public scanner and explains the exact bypass primitive, including Host: example.com/health?x= making request.url.path resolve to /health while the real request path reaches /protected BadHost.
    • likely: Direct-to-ASGI AI and MCP services are disproportionately exposed because many deployments put path-based API-key or admin gating in middleware.
    • unclear: Reviewed primary sources do not prove active exploitation in the wild; treat public scanner/PoC availability as exposure pressure, not confirmed exploitation.

    Impact Determination

    ClassificationCriteriaRequired evidenceRemediation triggerClosure condition
    Confirmed compromiseWeb/WAF logs show HTTP requests with path-delimiter characters (e.g. /, ?, #) in the Host header, followed by unauthorized access to administrative or restricted paths.Logs showing Host header values like example.com/health?x= targeting /admin with 200 OK status.Revoke all session keys, isolate internal API services, and run backend audits.Upgrade Starlette to 1.0.1 or later and verify backend logs for no further anomalous Host headers.
    Presumed exposedThe web application is built on FastAPI or Starlette with version < 1.0.1, exposing path-based middleware authorization gates.requirements.txt or poetry.lock lockfile showing starlette < 1.0.1 in active production environments.Upgrade the library dependency immediately to a patched release.Dependency verification outputs showing starlette >= 1.0.1 installed.
    Potentially exposedA Python project is running but lockfiles or backend web log records are not audited.Asset register indicating Python-based ASGI servers (FastAPI/Uvicorn) without version check.Execute the dependency and log audit script.Confirm if the asset is confirmed compromised, presumed exposed, or not exposed.
    Not exposedThe system utilizes non-affected framework stacks, or Starlette version is verified >= 1.0.1.Verified dependency files showing version 1.0.1 or later.None for this CVE.Version verification bundle is archived.

    Timeline

    • 2026-05-21: Starlette 1.0.1 patch is available according to downstream tracking references Tenable.
    • 2026-05-26: OSTIF publishes expanded BadHost disclosure because slow patch uptake and live vulnerable services raised ecosystem concerns OSTIF.
    • 2026-05-26: Tenable publishes/updates its CVE record with references to X41, OSTIF, GHSA, PyPA advisory data, the Starlette patch commit, and BadHost.org Tenable.
    • 2026-05-27: This article is refreshed to remove the unsupported KEV/active-exploitation framing and replace generic source links with exact primary and tracker references.

    What Happened

    Starlette handles URL reconstruction dynamically by concatenating the raw HTTP Host header with the request path to build a unified URI string. Because the Host header was not validated against standard RFC grammar, attackers could inject custom query delimiters (/health?x=) directly into the Host field:

    • The Request: GET /admin with Host: example.com/health?x=
    • The Reconstruction: Starlette builds the internal URL as http://example.com/health?x=/admin.
    • The Bypass: The underlying ASGI server processes the request path /admin, but Starlette’s routing/auth middleware inspects the reconstructed request.url.path which returns /health. If /health is whitelisted, the authentication check is bypassed entirely, granting unauthenticated access to the restricted /admin resources.

    Technical Analysis

    The fundamental error occurs during URL parsing inside Starlette’s utility functions. By failing to validate that the host contains only valid hostname characters, re-parsing the reconstructed URL causes the injection parameters to overwrite the target path, shifting the path boundary forward.

    Affected Assets and Blast Radius

    asset_selectors:
      - "starlette"
      - "fastapi"
      - "litellm"
      - "vllm"
    highest_value_assets:
      - "Internet-facing FastAPI applications utilizing path-based middleware"
      - "vLLM or LiteLLM AI endpoints exposed with API key authentication"
      - "Internal corporate admin dashboards written in Python ASGI frameworks"
    credentials_and_data_at_risk:
      - "Internal LLM access and model weights (via vLLM/LiteLLM bypass)"
      - "Backend database records accessible through administrative endpoints"
      - "Administrative session credentials and user data"

    Indicators And Detection Selectors

    vulnerabilities: ["CVE-2026-48710", "BadHost"]
    packages: ["starlette", "fastapi"]
    telemetry_selectors:
      - "starlette"
      - "fastapi"
      - "Host:"
      - "/health"

    Detection and Hunting

    Script: local repository and exported telemetry scope

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
    LOG_ROOT = os.environ.get("LOG_ROOT", "")
    OUT = Path(os.environ.get("OUT", "hp-starlette-cve-2026-48710-badhost-scope"))
    SINCE = "2026-05-26T00:00:00Z"
    UNTIL = "2026-05-26T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
    ]
    DOMAINS = [
      "request.url.path",
      "www.tenable.com",
    ]
    URLS = [
      "https://ostif.org/disclosing-the-badhost-vulnerability-in-starlette/",
      "https://www.tenable.com/cve/CVE-2026-48710",
      "https://badhost.org/",
      "http://example.com/health?x=/admin`",
    ]
    IPS = [
    ]
    HASHES = [
    ]
    PROCESS_PATTERNS = [
    ]
    NETWORK_PATTERNS = [
    ]
    
    # Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
    # Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
    
    OUT.mkdir(parents=True, exist_ok=True)
    indicators_file = OUT / "indicators.txt"
    
    # Collect unique indicators
    indicators = set()
    for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
        for val in group:
            if val:
                indicators.add(val)
    
    with open(indicators_file, "w") as f:
        for ind in sorted(indicators):
            f.write(ind + "\n")
    
    print(f"[+] Written unique selectors to {indicators_file}")
    
    # Walk local directory
    print(f"[+] Scanning directory: {ROOT} for selectors...")
    matches = []
    exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
    for root, dirs, filenames in os.walk(ROOT):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for filename in filenames:
            filepath = Path(root) / filename
            try:
                content = filepath.read_text(errors="ignore")
                for ind in indicators:
                    if ind in content:
                        matches.append(f"{filepath}: found '{ind}'")
            except Exception:
                pass
    
    if matches:
        (OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
        print(f"[!] Found {len(matches)} matches in codebase!")
    
    # Optional Log Scanning
    if LOG_ROOT and os.path.exists(LOG_ROOT):
        print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
        log_matches = []
        for root, _, filenames in os.walk(LOG_ROOT):
            for filename in filenames:
                filepath = Path(root) / filename
                try:
                    content = filepath.read_text(errors="ignore")
                    for ind in indicators:
                        if ind in content:
                            log_matches.append(f"{filepath}: found '{ind}'")
                except Exception:
                    pass
        if log_matches:
            (OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
            print(f"[!] Found {len(log_matches)} matches in logs!")
    
        if PACKAGES:
            registry_dir = OUT / "registry"
            registry_dir.mkdir(exist_ok=True)
    
    print(f"[+] Wrote scope artifacts under {OUT}")

    Remediation & Credential Rotation Plan

    Containment & Mitigation

    1. Host Header Validation: Ensure that front-facing reverse proxies (like Nginx, Apache, Caddy, Traefik, HAProxy, or Cloudflare) are explicitly configured to validate and normalize the Host header before passing the request downstream to Uvicorn/Starlette.
    2. Block Redirections: Add WAF rules to drop any incoming requests where the Host header contains non-standard URI characters such as /, ?, or #.

    Eradication & Recovery

    1. Upgrade Starlette Dependency: Mandate immediate dependency upgrades of Starlette to version 1.0.1 or later:
      • Command: pip install --upgrade starlette or poetry update starlette
    2. Rotate Exposed Secrets: If web logs show successful requests (200 OK) featuring a poisoned Host header targeting restricted administration directories, immediately revoke and rotate all backend API keys and session credentials accessible through the compromised services.

    Sources

    1. OSTIF: Disclosing the BADHOST Vulnerability in Starlette
    2. Tenable: CVE-2026-48710
    3. BadHost Scanner and Technical Overview
    4. GitHub Advisory: GHSA-86qp-5c8j-p5mr
    5. Starlette patch commit 764dab0