펜리젠트 헤더

CVE-2026-48710, BadHost in Starlette

CVE-2026-48710 is a Host header parsing flaw in Starlette that can turn a fragile middleware pattern into an authorization bypass. The issue is also known as BadHost. The affected condition is specific: Starlette before 1.0.1 could reconstruct request.url from an attacker-controlled 호스트 header in a way that made request.url.path differ from the path actually used for routing. If application code used that reconstructed URL path for security decisions, a malformed Host header could make protected routes look unprotected to middleware while the router still dispatched the request to the sensitive endpoint.(NVD)

That distinction matters. CVE-2026-48710 is not a universal remote code execution bug in Starlette. It is not proof that every FastAPI or Starlette application is exploitable. It is a framework-level inconsistency that becomes dangerous when applications build access control, allowlists, blocklists, tenant routing, SSRF guards, or admin protections on top of request.url 또는 request.url.path. Starlette 1.0.1 fixed the issue by ignoring malformed Host values when constructing request.url, using Host grammar aligned with RFC 9112 and RFC 3986, and falling back to scope["server"] when the Host value is invalid.(GitHub)

The official severity picture is still worth reading carefully. NVD lists the record as awaiting enrichment, while the GitHub advisory reports a CVSS 3.1 base score of 6.5, Medium. X41, the research team behind the public advisory, rated the issue High under CVSS 4.0, and the BadHost site uses stronger language because the bug can become severe in real deployments that protect high-value API paths with middleware. The right operational conclusion is not to argue over one adjective. Patch Starlette, inspect path-based security code, and verify whether malformed Host headers can reach your application.(NVD)

The facts security teams need first

항목What is known
CVECVE-2026-48710
Common nameBadHost
영향을 받는 구성 요소Starlette, the ASGI framework used directly and through frameworks such as FastAPI
영향을 받는 버전Starlette versions before 1.0.1 are the primary affected range described by the advisory and OSV record
Fixed versionStarlette 1.0.1
취약성 등급Inconsistent interpretation of HTTP request data, mapped by NVD to CWE-444
Typical vulnerable patternMiddleware or application code using request.url 또는 request.url.path to make security-sensitive decisions
Typical impactPath-based authorization bypass, security middleware bypass, and downstream impact depending on what the protected route can do
Typical non-impact caseApplications that enforce authorization at route or endpoint level and do not rely on reconstructed URL strings for access control
Main fixUpgrade Starlette and remove fragile path-string authorization logic
Important compensating controlA fronting proxy or gateway that rejects malformed Host headers before they reach the ASGI app, provided forwarded Host headers are not attacker-controlled

GitHub’s advisory states the core issue plainly: Starlette did not validate the HTTP 호스트 header before using it to reconstruct request.url. An invalid Host containing reserved URI delimiters could cause the parsed URL path to differ from the path actually requested. The router would still use the raw request path, but middleware reading request.url.path could see a different path.(GitHub)

Starlette’s release notes for 1.0.1 list the fix as ignoring malformed Host headers when constructing request.url. The corresponding commit adds Host validation and includes tests for invalid Host values containing characters such as /, ?, #, @, backslash, and spaces. Those characters are important because they can alter how a URI parser divides authority, path, query, and fragment components.(starlette.io)

Why this bug exists at the HTTP boundary

To understand CVE-2026-48710, start with the difference between the HTTP request target and the Host header.

A normal HTTP/1.1 origin-form request looks like this:

GET /admin/settings?tab=users HTTP/1.1
Host: api.example.com

The request target is /admin/settings?tab=users. The Host header is api.example.com. They are separate pieces of data. RFC 9112 requires clients to send Host in HTTP/1.1 requests and requires servers to reject requests that are missing Host, contain more than one Host field, or contain an invalid Host field value. The same specification explains that the origin-form request target carries the path and optional query, while Host supplies authority information.(RFC 편집기)

RFC 3986 defines the authority component as the part following // and ending at the next /, ?, #, or the end of the URI. That means those delimiter characters are not harmless inside Host-like authority text. If a framework concatenates a Host header with a path and then gives the result to a URI parser, an attacker-controlled delimiter inside Host can change where the parser thinks the authority ends and where the path or query begins.(RFC 편집기)

ASGI adds another important layer. In the ASGI HTTP scope, 경로 is a decoded path value excluding the query string, raw_path may preserve the original path bytes, query_string stores the query string, and 헤더 carries request headers. A framework can expose both low-level scope values and higher-level convenience objects such as request.url. The security problem appears when two layers derive different meanings from the same request.(ASGI Documentation)

In Starlette before 1.0.1, the router and request.url did not always agree when the Host header was malformed. Routing relied on the actual request path. URL reconstruction used the Host header and path together. When Host was valid, that was normal and useful. When Host contained URI delimiters, the reconstructed URL could parse differently from the actual request path.(GitHub)

The exploit is path confusion, not router control

How BadHost Creates Path Confusion in Starlette

The vulnerable mental model looks like this:

if request.url.path.startswith("/admin"):
    require_admin_user()

That code assumes request.url.path is the same path the Starlette router used to select the endpoint. CVE-2026-48710 breaks that assumption when the Host header is malformed and reaches Starlette.

The GitHub advisory gives a simple example. A request may be routed as /foo, while the reconstructed URL parses as a different path because a malformed Host header moves the real path into another URI component. The X41 advisory shows the same class of mismatch and demonstrates that middleware can make an authorization decision based on the reconstructed path while the router still dispatches the actual requested path.(GitHub)

A simplified vulnerable Starlette middleware might look like this:

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse

class AdminPathGate(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        if request.url.path.startswith("/admin"):
            token = request.headers.get("authorization")
            if token != "Bearer expected-admin-token":
                return JSONResponse({"detail": "forbidden"}, status_code=403)

        return await call_next(request)

The code is not malicious. It is also easy to understand why a developer might write it. Middleware sits before the endpoint, the admin area lives under /adminrequest.url.path looks like the obvious property to inspect. But in vulnerable Starlette versions, request.url.path was not safe as a security boundary if a malformed Host header could influence URL reconstruction.

The safer design is not just to replace one string with another. Long-term access control should be attached to routes, endpoints, dependencies, or authorization policies that are not derived from a reconstructed URL string. For FastAPI, endpoint-level dependencies are a common way to bind authorization to the actual route being executed:

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()

async def require_admin(authorization: str | None = Header(default=None)) -> None:
    if authorization != "Bearer expected-admin-token":
        raise HTTPException(status_code=403, detail="forbidden")

@app.get("/admin/settings", dependencies=[Depends(require_admin)])
async def admin_settings():
    return {"status": "ok"}

That pattern does not ask a reconstructed URL object whether the route is sensitive. The protected route declares its own requirement. Standard FastAPI security dependencies and route-level enforcement are not the same as a custom middleware that performs path-prefix authorization using request.url.path. FastAPI is built on Starlette, so dependency versions still matter, but the vulnerable application pattern is narrower than “any FastAPI app.”(FastAPI)

As a temporary reduction of this specific risk, Starlette code that absolutely must inspect the request path should prefer ASGI scope values over request.url.path:

path = request.scope.get("path", "")

if path.startswith("/admin"):
    require_admin_user()

That reduces exposure to this Host-based URL reconstruction bug, but it is not a complete authorization architecture. Path-string checks still have other problems: trailing slashes, normalization, percent encoding, route aliases, mounted sub-applications, case behavior, reverse proxy rewrites, and future refactors. Starlette’s maintainer explicitly warned that authorization should not be based on request path, host, or query string when better route-bound options exist.(Marcelo Trylesinski)

A minimal request-level example

Consider this request:

GET /admin/panel HTTP/1.1
Host: example.com/?x=
Connection: close

The actual request target is /admin/panel. A router that uses the request path can dispatch the admin endpoint. But if the application reconstructs a URL by concatenating scheme, Host, and path, then parses it as a URI, the ? inside Host can make the parser interpret later data as query text rather than as the path the application expected.

That is the essence of BadHost. The attacker does not need to change the route. The attacker needs to change what security middleware thinks the route is.

X41’s public proof of concept shows a route protected by middleware that returns 403 when the path begins with /admin. A normal request receives 403. A request using a malformed Host header reaches the same protected endpoint but receives 200 because the middleware reads a different request.url.path.(x41-dsec.de)

For authorized internal testing, a small raw HTTP probe can help confirm whether malformed Host headers reach your service and whether the response differs from the control case. Keep probes narrow, run them only against systems you own or are explicitly authorized to test, and do not turn this into internet-wide scanning.

#!/usr/bin/env python3
import socket
import ssl
import sys
from urllib.parse import urlparse

def send_raw(url: str, host_header: str) -> bytes:
    parsed = urlparse(url)
    scheme = parsed.scheme or "https"
    port = parsed.port or (443 if scheme == "https" else 80)
    hostname = parsed.hostname
    path = parsed.path or "/"
    if parsed.query:
        path += "?" + parsed.query

    request = (
        f"GET {path} HTTP/1.1\r\n"
        f"Host: {host_header}\r\n"
        "User-Agent: authorized-badhost-check/1.0\r\n"
        "Connection: close\r\n"
        "\r\n"
    ).encode("ascii", "replace")

    with socket.create_connection((hostname, port), timeout=5) as sock:
        if scheme == "https":
            ctx = ssl.create_default_context()
            with ctx.wrap_socket(sock, server_hostname=hostname) as tls:
                tls.sendall(request)
                return tls.recv(4096)
        sock.sendall(request)
        return sock.recv(4096)

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("usage: python badhost_check.py https://api.example.com/admin/panel 'api.example.com/?x='")
        raise SystemExit(2)

    response = send_raw(sys.argv[1], sys.argv[2])
    print(response.decode("iso-8859-1", "replace"))

A meaningful test is comparative. Send a valid Host request first. Confirm the expected authentication behavior. Then send a malformed Host request and confirm whether the service rejects it at the edge, rejects it in the application, or changes behavior. A single 200 response is not enough to prove impact unless you also show the protected route, authentication expectation, and why the response demonstrates a bypass.

Who is actually exposed

CVE-2026-48710 Exposure and Remediation Workflow

CVE-2026-48710 exposure is best judged with a decision table rather than a yes-or-no dependency check.

ConditionRisk meaningWhat to verify
Starlette is older than 1.0.1The framework behavior may be vulnerableCheck the runtime package version, not only the source tree
The app uses FastAPIFastAPI depends on Starlette, but exploitability depends on application code and dependency versionInspect resolved dependencies and custom middleware
Middleware uses request.url.path for authHigh-risk patternSearch code for path-prefix authorization and access gates
Middleware uses request.url for allowlists, tenant routing, SSRF guards, or callback checksPotentially high impactDetermine whether the decision protects sensitive behavior
A reverse proxy rejects malformed Host before forwardingStrong mitigating controlTest whether invalid Host reaches the ASGI server
The app trusts X-Forwarded-Host from untrusted clientsPossible bypass of proxy-level assumptionsConfirm trusted proxy configuration and header stripping
Authorization is route-bound or endpoint-boundLower risk from this bug classVerify no earlier middleware creates conflicting allow/deny logic
Sensitive AI, admin, or internal control endpoints existImpact can rise even if the base CVSS is mediumMap protected actions reachable behind path gates

The most dangerous pattern is a direct ASGI deployment or a permissive reverse proxy in front of a Starlette or FastAPI application that uses custom middleware for path-level access control. That includes internal services if the internal network is not a trusted boundary. Modern incidents repeatedly show that “internal only” often means “reachable from CI runners, staging networks, compromised workloads, developer VPNs, service mesh peers, or SSRF primitives.”

The least concerning pattern is a patched Starlette deployment, with strict edge Host validation, route-bound authorization, and no security decisions based on reconstructed URL strings. Even then, it is worth adding regression tests because the same design mistake can reappear in future middleware.

Why FastAPI and AI infrastructure deserve special attention

FastAPI is compatible with Starlette and is built on top of it; FastAPI’s FastAPI class is a subclass of Starlette. That does not mean every FastAPI service is exploitable, but it does mean Starlette dependency state matters for FastAPI deployments.(FastAPI)

The BadHost site highlights AI-adjacent infrastructure because many LLM APIs, inference gateways, agent servers, MCP services, and internal developer tools are written with FastAPI or Starlette. That observation should be handled with precision. CVE-2026-48710 is not an LLM vulnerability. It does not compromise a model because the model is large or because the endpoint uses AI. The concern is that AI infrastructure often exposes powerful actions through Web APIs: model management, file access, tool invocation, workflow execution, admin dashboards, token management, connector configuration, and internal proxying. If those actions sit behind fragile path-based middleware, the business impact of a bypass can be much higher than the framework bug sounds in isolation.(CVE-2026-48710 – Nemesis – BadHost)

MCP servers and agent gateways make this especially interesting for defenders. An agent control plane may have endpoints that are boring in a normal CRUD app but dangerous in an automation context: run a tool, fetch a URL, read a connector, execute a workflow, upload a plugin, rotate credentials, or change a model provider key. A path confusion flaw near that control plane can turn a medium framework issue into a serious operational incident.

The practical response is to inventory AI-facing ASGI services before lower-value applications. Prioritize anything that has one or more of these traits:

Service traitWhy it increases priority
Exposes admin or operator pathsPath-based middleware is common around admin prefixes
Executes tools or workflowsA bypass may lead to actions beyond data exposure
Manages API keys, model keys, or connector tokensConfidentiality impact can be high
Runs in a flat internal networkInternal attackers or SSRF may reach it
Uses custom auth middlewareCustom path checks are more likely than in standard dependency-based auth
Trusts forwarded headersHost assumptions may be weaker than expected
Is deployed directly with Uvicorn or another ASGI serverEdge rejection of malformed Host may be absent

The safest sentence for engineering teams is simple: do not assume “we use FastAPI” means vulnerable, and do not assume “we are behind a proxy” means safe. Check the version, the middleware, the edge behavior, and the authorization model.

What Starlette changed in 1.0.1

Starlette 1.0.1 changed how malformed Host values affect URL construction. The fix ignores invalid Host headers when constructing request.url and falls back to server information from the ASGI scope. The patch adds validation to prevent Host values containing characters that can make urlsplit produce a path different from scope["path"]. The tests added in the patch cover values such as foo/?x=, foo/#, foo/bar, user@foo, foo\barfoo bar.(GitHub)

That patch is the correct foundation because it fixes the framework-level inconsistency. But the application-level lesson remains: security code should not depend on a reconstructed URL string when more authoritative route or scope data is available. Frameworks can harden dangerous edge cases, but they cannot rescue every fragile authorization pattern.

A dependency check can identify candidates for urgent review:

python - <<'PY'
import importlib.metadata as md
from packaging.version import Version

try:
    version = md.version("starlette")
except md.PackageNotFoundError:
    print("starlette is not installed in this Python environment")
    raise SystemExit(0)

print(f"starlette {version}")
if Version(version) >= Version("1.0.1"):
    print("patched for CVE-2026-48710 according to the upstream fixed version")
else:
    print("needs review: upgrade Starlette and inspect path-based security code")
PY

For package managers, the upgrade may look like one of these, depending on your project:

python -m pip install --upgrade "starlette>=1.0.1"

python -m pip install --upgrade "fastapi" "starlette>=1.0.1"

poetry add "starlette>=1.0.1"

uv add "starlette>=1.0.1"

Treat those commands as examples, not universal deployment instructions. FastAPI and other frameworks may pin compatible Starlette ranges. Your lockfile, test suite, and runtime environment decide what actually ships. The OSV record identifies Starlette 1.0.1 as the fixed PyPI version, while distribution packages may have backported fixes without matching PyPI’s exact version number.(osv.dev)

Static review, find the dangerous patterns

A version check tells you whether the framework bug exists. It does not tell you whether your application had exploitable security logic. Code review is the next step.

Start with a broad search:

rg -n "request\.url|request\.url\.path|str\(request\.url\)|URL\(scope=" .
rg -n "BaseHTTPMiddleware|@app\.middleware\(['\"]http['\"]\)|Middleware\(" .
rg -n "startswith\(['\"]/|in \[['\"]/|path_prefix|allowlist|denylist|blacklist|whitelist" .
rg -n "x-forwarded-host|forwarded|trustedhost|proxy_headers|root_path" .

Look for security decisions, not just references. Logging request.url is usually not exploitable by itself. Building a redirect from request.url can be risky, but the BadHost authorization bypass pattern is strongest when a branch controls access:

if request.url.path.startswith("/internal"):
    enforce_internal_auth()

if request.url.path not in PUBLIC_PATHS:
    require_login()

if request.url.path.startswith("/api/admin") and not user.is_admin:
    return forbidden()

if "/webhook/" in request.url.path:
    skip_csrf_check()

Each of those patterns deserves attention because a mismatch between routed path and reconstructed URL path can change security behavior. The same review should include tenant routing, callback validation, SSRF prevention, and proxy dispatch if those decisions depend on the reconstructed URL.

A more structured review table helps triage findings:

Code patternRisk level중요한 이유Preferred change
request.url.path.startswith("/admin") before auth높음Can skip auth if reconstructed path differsUse route-bound or endpoint-bound authorization
request.url.path in PUBLIC_PATHS to skip login높음Sensitive route may appear publicAttach public/private status to route definitions
str(request.url) used for security redirectsMediumMay create host or path confusionUse configured canonical origins and explicit redirect targets
request.url.hostname used for tenant trustMedium to highHost is client-controlled unless strictly validatedUse verified tenant mapping after edge validation
request.scope["path"] used for temporary gateLower for BadHostNot affected by reconstructed Host URL mismatchStill prefer route-bound policy
request.url used only in logs낮음May pollute logs but does not decide accessSanitize logs and keep edge rejection

If you use Semgrep, a first-pass local rule can flag the riskiest shape. This is intentionally broad and should be tuned for your codebase:

rules:
  - id: starlette-request-url-path-security-check
    languages: [python]
    severity: WARNING
    message: "Review security logic based on request.url.path. In Starlette before 1.0.1 this may be affected by CVE-2026-48710, and path-string authorization is fragile even after patching."
    patterns:
      - pattern-either:
          - pattern: |
              if $REQ.url.path.startswith($PREFIX):
                  ...
          - pattern: |
              if $REQ.url.path in $PATHS:
                  ...
          - pattern: |
              if $REQ.url.path == $PATH:
                  ...
          - pattern: |
              if $PATH in $REQ.url.path:
                  ...

The rule will produce false positives. That is acceptable for triage. The goal is to find places where a reviewer can answer three concrete questions:

  1. Does this code make a security decision?
  2. Can an attacker influence the Host or forwarded Host seen by the app?
  3. Does the protected endpoint have meaningful confidentiality, integrity, or availability impact?

Behavioral validation in a safe environment

A good CVE-2026-48710 validation has three parts: dependency evidence, code evidence, and behavioral evidence.

Dependency evidence answers whether a vulnerable Starlette version is present. Code evidence answers whether the application uses a vulnerable pattern. Behavioral evidence answers whether the malformed Host actually changes security behavior in the deployed environment.

A clean test plan looks like this:

단계TestExpected safe resultRisky result
1Request protected path with normal Host and no auth401 or 403200 or sensitive response
2Request same path with malformed Host and no auth400 at edge, or same 401/403200 or different access behavior
3Request same path with valid authExpected authorized responseConfirms route is real
4Repeat through public edge and internal service addressEdge should reject or app should behave consistentlyInternal path differs from edge behavior
5Check application logsRejected malformed Host or consistent auth loggingRoute handler executed while auth middleware saw different path

Avoid testing random hosts on the internet. This bug is easy to probe, and large-scale probing can look like exploitation. In a bug bounty program, stay within scope and include a minimal number of requests.

If you run AI-assisted validation, keep it evidence-based. Penligent’s AI pentesting material emphasizes checking exposure, affected version ranges, reachable conditions, patch state, and safe behavioral evidence rather than relying only on banners or version strings. That discipline fits CVE-2026-48710 well: a useful result should connect dependency state, middleware code, edge behavior, and reproducible request evidence. Penligent’s platform is built around guided AI-assisted penetration testing, tool orchestration, verification, and reporting, which can be useful for keeping this kind of retest consistent across many services without turning the finding into an unreviewed scanner alert.(펜리전트)

Reverse proxies help, but do not end the review

The GitHub advisory notes that proxies can mitigate the issue if they reject or normalize malformed Host headers before forwarding, and if the application does not trust attacker-controlled X-Forwarded-Host or similar headers elsewhere. That caveat is important. A reverse proxy is a strong control only when its behavior is known, tested, and paired with correct forwarded-header trust boundaries.(GitHub)

A safe edge posture usually includes these practices:

제어목적Common failure
Host allowlistOnly expected hostnames reach the appWildcard hosts or default server blocks forward unexpected traffic
Invalid Host rejectionMalformed values never reach StarletteProxy forwards raw Host to backend
Header strippingClient-supplied X-Forwarded-Host is removedBackend trusts attacker-controlled forwarded headers
Canonical origin configApp uses configured public origin for redirects and linksApp derives trust from request Host
Backend isolationASGI server is not directly reachableInternal service port exposed to VPN, pod network, or staging users
회귀 테스트Edge behavior remains stable across config changesProxy upgrade or ingress change reopens malformed Host path

For Nginx-style deployments, prefer an explicit server name and avoid forwarding arbitrary Host values to the application unless the application truly needs multi-tenant host behavior. A simplified pattern is:

server {
    listen 443 ssl;
    server_name api.example.com;

    location / {
        proxy_set_header Host api.example.com;
        proxy_set_header X-Forwarded-Host api.example.com;
        proxy_set_header X-Forwarded-Proto https;
        proxy_pass http://starlette_backend;
    }
}

That example is intentionally strict. Some applications need the original Host for tenant routing. If so, validate it against known tenants at the edge and again in application code. Do not accept arbitrary Host values and later treat them as trusted security context.

For HAProxy-style deployments, the control often appears as a deny rule for malformed Host values and an allowlist for known domains:

acl host_has_bad_delimiter req.hdr(Host) -m reg "[/?#@\\ ]"
http-request deny deny_status 400 if host_has_bad_delimiter

acl allowed_host req.hdr(Host) -i api.example.com
http-request deny deny_status 421 unless allowed_host

The exact syntax should be tested against your proxy version and configuration style. The goal is not to copy a snippet blindly. The goal is to guarantee that Host values containing URI delimiters cannot reach the ASGI application.

Forwarded headers can reopen the door

Many ASGI deployments sit behind a proxy that rewrites connection information. That is normal. The danger appears when the backend trusts forwarded headers from any client instead of only from a known proxy.

If a backend or middleware uses X-Forwarded-Host to reconstruct URLs, then an edge that rejects malformed 호스트 may still be bypassed if the attacker can send a malformed X-Forwarded-Host and the application treats it as authoritative. The GitHub advisory explicitly limits proxy mitigation to cases where the app does not trust attacker-controlled forwarded Host values.(GitHub)

Review these settings and code paths:

rg -n "X-Forwarded-Host|Forwarded|TrustedHost|ProxyHeaders|proxy_headers|forwarded_allow_ips|root_path" .

Then answer:

질문Safe answer
Who can set X-Forwarded-Host?Only the trusted edge proxy after stripping client-supplied copies
Does the app trust proxy headers from all IPs?아니요
Is the ASGI server directly reachable?아니요
Does the proxy normalize or reject malformed Host?Yes, verified with tests
Does application auth depend on reconstructed URL path?아니요

Forwarded headers are not automatically bad. They are necessary in many deployments. They become dangerous when they are treated as authenticated facts while still being client-controlled input.

Logging and detection

CVE-2026-48710 leaves several useful signals. Some appear at the edge, some in application logs, and some only after you add instrumentation.

A malformed Host header is the most obvious indicator. Host values containing /, ?, #, @, backslash, or spaces should be rare in normal production traffic. They are strong candidates for rejection and alerting. Starlette’s patch tests specifically include these categories because they can alter URL parsing.(GitHub)

Useful detection signals include:

신호Where to look중요한 이유Caveat
Host contains /, ?, #, @, backslash, or spaceEdge proxy logs, WAF logs, app logsDirect probe indicatorSome logs may normalize or omit invalid headers
Protected path returns 2xx without authApp access logs, auth logsPossible bypassMust compare with expected auth behavior
App logs show routed path and request.url.path differCustom instrumentationDirect evidence of desyncRequires logging both values
400 or 421 for malformed HostEdge proxy logsHealthy rejectionConfirm backend never receives the request
Untrusted X-Forwarded-Host with delimitersProxy and app logsPossible alternate injection pathHeader may be stripped before logging
Sudden probes across /admin, /internal, /metrics, /debugEdge logsRecon or exploit attemptCommon scanner noise, correlate with Host anomalies

A temporary diagnostic middleware in a staging environment can reveal mismatches:

from starlette.middleware.base import BaseHTTPMiddleware

class PathDiagnosticMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        scope_path = request.scope.get("path")
        url_path = request.url.path

        if scope_path != url_path:
            # Use your structured logger here. Do not log secrets.
            print({
                "event": "path_mismatch",
                "scope_path": scope_path,
                "url_path": url_path,
                "host": request.headers.get("host"),
                "x_forwarded_host": request.headers.get("x-forwarded-host"),
            })

        return await call_next(request)

Do not leave noisy diagnostic logging in production without review. Host headers may contain attacker-controlled content, and logs can become an injection surface for downstream tools. For production, prefer structured logging, truncation, sanitization, and alerting at the edge.

Dependency reality, PyPI, lockfiles, containers, and distributions

Python dependency state is often messier than a single pip show starlette.

A service may use Starlette directly. It may use FastAPI, which depends on Starlette. It may vendor part of a framework. It may run from a container image built weeks before the lockfile changed. It may use a Linux distribution package with a backported fix. It may also run multiple Python environments across workers, background jobs, and admin servers.

A reliable inventory should include:

python -m pip show starlette fastapi

python -m pip freeze | rg "starlette|fastapi"

python - <<'PY'
import importlib.metadata as md
for pkg in ["starlette", "fastapi", "uvicorn"]:
    try:
        print(pkg, md.version(pkg))
    except md.PackageNotFoundError:
        print(pkg, "not installed")
PY

For Poetry:

poetry show starlette fastapi
poetry lock --no-update

For uv:

uv pip list | rg "starlette|fastapi"
uv tree | rg "starlette|fastapi"

For containers:

docker run --rm your-image:tag python -m pip show starlette fastapi
docker run --rm your-image:tag python - <<'PY'
import importlib.metadata as md
print("starlette", md.version("starlette"))
PY

For SBOM workflows, search the generated CycloneDX or SPDX artifact for Starlette:

rg -n '"name": "starlette"|"name":"starlette"|PackageName: starlette|starlette' sbom.*

Distribution packages need special care. Debian’s tracker, for example, distinguishes vulnerable and fixed package states across releases, including security backports. A Debian package may not show the same version number as PyPI’s fixed release while still carrying a fix, or it may remain vulnerable in a release line until the security update is applied.(security-tracker.debian.org)

The operational rule is simple: verify the runtime artifact. The lockfile is evidence, not proof. The container image, host package, and running Python environment are what matter.

Why the same bug gets different severity labels

CVE-2026-48710 is a good example of why CVSS is useful but not sufficient.

GitHub’s advisory scores the issue as CVSS 3.1 6.5 Medium. NVD displays the record as awaiting enrichment and maps the weakness to CWE-444. X41 rates the advisory High using CVSS 4.0. Public BadHost material uses stronger language because the issue can affect middleware in applications that protect sensitive routes.(NVD)

Those views can coexist because they answer different questions.

보기What it capturesWhat it may understate
Framework base scoreThe generic vulnerability in StarletteBusiness impact of the protected endpoint
Research advisory scoreLikely exploitability and common deployment patternsWhether a given app has the vulnerable middleware pattern
Public impact framingWorst-case composition across popular stacksThe fact that many apps are not exploitable
Internal risk ratingYour actual endpoint, auth model, exposure, and dataHard to compare across organizations

A medium base score can still require urgent action if the vulnerable path controls admin functions, secrets, agent tools, file access, deployment actions, or internal proxying. A high public advisory can still be non-exploitable in a particular environment if Starlette is patched, the edge rejects malformed Host, and authorization is endpoint-bound.

Good security teams avoid both extremes. They do not dismiss the issue because the base score is medium. They also do not declare a breach because a dependency is present. They test the actual chain.

Related CVE, Apache HTTP Server CVE-2023-25690

CVE-2026-48710 belongs to a broader family of bugs where different components interpret request data differently. A useful comparison is CVE-2023-25690 in Apache HTTP Server.

Apache describes CVE-2023-25690 as an HTTP request smuggling issue affecting some mod_proxy configurations in Apache HTTP Server 2.4.0 through 2.4.55. The vulnerable condition involved configurations where RewriteRule 또는 ProxyPassMatch used a non-specific pattern and reinserted user-controlled request-target data into the proxied request. Apache states that this could lead to request splitting or smuggling, bypass access controls, proxy unintended URLs, and poison caches. The fix shipped in Apache HTTP Server 2.4.56.(httpd.apache.org)

The two CVEs are not the same bug. CVE-2023-25690 is an Apache proxy configuration issue. CVE-2026-48710 is a Starlette URL reconstruction issue triggered by malformed Host values and dangerous application assumptions. But they rhyme at the architectural level:

AspectCVE-2026-48710CVE-2023-25690
Main componentStarletteApache HTTP Server with specific proxy rewrite configurations
Core themeReconstructed URL path differs from routed pathProxy rewrite can reinterpret user-controlled request-target data
Typical impactMiddleware authorization bypass or security logic bypassRequest smuggling, access control bypass, unintended proxying, cache poisoning
Main fixUpgrade Starlette and avoid request.url.path security gatesUpgrade Apache and avoid unsafe proxy rewrite patterns
Broader lessonDo not base trust on reconstructed request stringsDo not reinsert ambiguous user-controlled request-target data into downstream requests

The shared lesson is stronger than either CVE alone: the path a security check sees must be the same path the application, proxy, cache, and upstream service will act on. Any gap between those interpretations can become an attack surface.

Hardening application authorization

The best fix for CVE-2026-48710 is to upgrade Starlette. The best fix for the design class is to remove path-string authorization from generic middleware.

A stronger FastAPI pattern binds authorization to the endpoint:

from fastapi import Depends, FastAPI, HTTPException, Request

app = FastAPI()

async def current_user(request: Request) -> dict:
    token = request.headers.get("authorization")
    if token != "Bearer expected-token":
        raise HTTPException(status_code=401, detail="missing or invalid token")
    return {"sub": "user-123", "role": "user"}

async def require_admin(user: dict = Depends(current_user)) -> None:
    if user.get("role") != "admin":
        raise HTTPException(status_code=403, detail="admin required")

@app.get("/admin/audit-log", dependencies=[Depends(require_admin)])
async def audit_log():
    return {"events": []}

@app.get("/health")
async def health():
    return {"ok": True}

The public route is public because it has no admin dependency. The admin route is protected because its route declaration says so. There is no global middleware trying to infer sensitivity from a reconstructed string.

For Starlette, route-level structure can accomplish the same idea. The exact implementation depends on your authentication system, but the policy should attach to route definitions, endpoint metadata, or explicit permission checks rather than prefix guesses.

If you must keep middleware during migration, make it conservative:

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse

SENSITIVE_PREFIXES = ("/admin", "/internal", "/ops")

class TemporaryScopePathGate(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        path = request.scope.get("path", "")

        if path.startswith(SENSITIVE_PREFIXES):
            token = request.headers.get("authorization")
            if token != "Bearer expected-admin-token":
                return JSONResponse({"detail": "forbidden"}, status_code=403)

        return await call_next(request)

Then add tests that prove the policy holds for malformed Host values:

from starlette.testclient import TestClient

def test_admin_rejects_malformed_host_without_auth(app):
    client = TestClient(app)
    response = client.get(
        "/admin/audit-log",
        headers={"host": "example.com/?x="},
    )
    assert response.status_code in {400, 401, 403}

The exact expected status depends on whether the framework, proxy simulation, or app rejects the request first. The important assertion is that the protected endpoint does not return a successful unauthenticated response.

Hardening URL and origin handling beyond this CVE

BadHost is about request.url.path, but the same input trust problem often appears in other places.

Applications frequently use request-derived URLs for redirects, password reset links, callback validation, tenant selection, CORS decisions, webhook routing, and SSRF controls. CVE-2026-48710 should trigger a broader audit of how your application treats Host and URL-derived values.

Review code like this:

reset_url = str(request.url_for("reset_password", token=token))

if request.url.hostname.endswith(".trusted.example"):
    allow_tenant_action()

if request.url.path.startswith("/webhook/"):
    skip_csrf()

return RedirectResponse(str(request.url.replace(path="/login")))

Some of that code may be safe in context. Some may be risky. The question is whether the value is being used for display, routing, policy, or trust.

A hardened approach uses configured canonical origins:

from urllib.parse import urljoin

PUBLIC_ORIGIN = "https://app.example.com"

def build_public_url(path: str) -> str:
    if not path.startswith("/"):
        raise ValueError("path must be absolute")
    return urljoin(PUBLIC_ORIGIN, path)

For tenant routing, use a validated mapping:

ALLOWED_TENANT_HOSTS = {
    "tenant-a.example.com": "tenant_a",
    "tenant-b.example.com": "tenant_b",
}

def tenant_from_host(host: str) -> str:
    try:
        return ALLOWED_TENANT_HOSTS[host.lower()]
    except KeyError:
        raise PermissionError("unknown tenant host")

For security-sensitive behavior, prefer explicit configuration and verified context over request-derived reconstruction. Host is a routing hint from the client until your edge and application validate it.

Patch and retest plan for production teams

A practical remediation plan should avoid two common mistakes. The first mistake is patching the package but leaving vulnerable middleware in place. The second is rewriting middleware but leaving a vulnerable Starlette version exposed to future code paths.

Use a two-track plan.

Track액션인증
FrameworkUpgrade Starlette to 1.0.1 or a downstream fixed packageRuntime version check and dependency lock review
ApplicationRemove request.url.path security gatesCode review and regression tests
EdgeReject malformed Host and restrict allowed hostsRaw request tests through public ingress
Forwarded headersStrip and reset X-Forwarded-Host at trusted proxyConfirm backend cannot receive client-supplied forwarded Host
모니터링Alert on malformed Host probesEdge logs and application logs
ReportingAttach evidence to ticketDependency, code, request, response, and fix proof

A deployment checklist can look like this:

[ ] Identify every service running Starlette directly or through FastAPI.
[ ] Confirm the runtime Starlette version inside the deployed artifact.
[ ] Upgrade to Starlette 1.0.1 or a vendor package with the backported fix.
[ ] Search for request.url.path and request.url in security-sensitive code.
[ ] Replace path-string authorization with route-bound or endpoint-bound checks.
[ ] Add regression tests for malformed Host values against sensitive paths.
[ ] Confirm edge proxy rejects malformed Host headers.
[ ] Confirm client-supplied X-Forwarded-Host is stripped or ignored.
[ ] Retest public ingress and internal service paths.
[ ] Keep evidence with the change request or incident ticket.

For bug bounty hunters and red teamers, a high-quality report should include the same structure. Do not stop at “Host header can contain a question mark.” Show the affected Starlette version, the vulnerable middleware or behavior, the control request, the malformed Host request, the difference in authorization outcome, and the business impact of the reached endpoint. If the target is behind a proxy, document whether the proxy forwarded the malformed Host or whether an alternate forwarded Host path was involved.

Common mistakes during remediation

The most common remediation mistake is treating this as a simple dependency alert. A dependency alert is valuable, but it does not prove exploitability and it does not remove fragile authorization logic. If the same code later runs behind a different proxy, or a future framework bug creates another mismatch, the design flaw remains.

The second mistake is trusting the proxy without testing it. Many teams assume Nginx, Apache, an ingress controller, a cloud load balancer, or an API gateway rejects malformed Host values. Some do. Some configurations do not. Some reject at the public edge but allow direct access to the backend service. Some reject 호스트 but pass X-Forwarded-Host. Test the path your traffic actually takes.

The third mistake is using a denylist of bad paths. For example:

if not request.url.path.startswith("/public"):
    require_login()

This can fail in surprising ways even beyond CVE-2026-48710. It is safer to make sensitive endpoints explicitly require authorization than to infer public and private state from path strings.

The fourth mistake is ignoring internal services. BadHost is easy to think of as a public internet issue, but internal AI gateways, admin APIs, and staging services often have weaker edge controls and stronger privileges. Internal does not mean harmless.

The fifth mistake is overclaiming. A scanner result that says “Starlette < 1.0.1” is a lead, not a final vulnerability report. A malformed Host response difference is suspicious, but the meaningful question is whether a protected action became reachable without the required security condition.

자주 묻는 질문

Is every FastAPI app vulnerable to CVE-2026-48710?

  • No. FastAPI is built on Starlette, so FastAPI deployments should check their resolved Starlette version, but exploitability depends on application behavior.
  • The highest-risk pattern is custom middleware that uses request.url.path 또는 request.url to decide whether authentication, authorization, CSRF checks, tenant rules, or other security controls apply.
  • FastAPI endpoint dependencies and route-bound security checks are not the same as fragile path-prefix middleware.
  • Patch Starlette anyway. A non-exploitable app today can become exploitable later if new middleware or deployment paths are added.

What exactly does Starlette 1.0.1 change?

  • Starlette 1.0.1 ignores malformed Host headers when constructing request.url.
  • The patch validates Host grammar so characters that can alter URL parsing, such as /, ?, #, @, backslash, or spaces, do not create a reconstructed URL path different from scope["path"].
  • If Host is malformed, Starlette falls back to server information from the ASGI scope rather than trusting the invalid Host value.
  • The fix addresses the framework inconsistency, but applications should still avoid authorization based on reconstructed URL strings.(GitHub)

Can a reverse proxy fully mitigate BadHost?

  • A reverse proxy can be a strong mitigation if it rejects malformed Host headers before they reach the ASGI application.
  • The mitigation is incomplete if the backend is directly reachable, if the proxy forwards arbitrary Host values, or if the app trusts client-supplied X-Forwarded-Host.
  • You should test the public ingress and any internal backend path separately.
  • Even with a strong proxy, upgrade Starlette and remove fragile path-based security logic. Defense in depth is better than relying on one layer.

Why do different sources score the same issue differently?

  • GitHub’s advisory gives a CVSS 3.1 score of 6.5 Medium, while X41 rates the issue High under CVSS 4.0, and public BadHost material uses stronger impact language.
  • The difference comes from context. The base framework issue is conditional, but the impact can rise sharply when the bypass protects admin functions, AI agent actions, internal proxy routes, secrets, or RCE-capable operations.
  • Internal severity should be based on your environment: affected version, reachable malformed Host, vulnerable middleware, and the value of the protected endpoint.
  • Do not dismiss the issue only because one source says Medium, and do not claim compromise only because a dependency is present.

Should I search only for request.url.path?

  • No. request.url.path is the most obvious pattern, but related security decisions may use request.url, str(request.url), request.url.hostname, URL replacement helpers, tenant routing logic, or forwarded Host middleware.
  • Search for BaseHTTPMiddleware, @app.middleware("http"), allowlist and denylist logic, public path lists, admin path prefixes, and forwarded header handling.
  • Separate harmless logging from security decisions. A log statement is not the same as an authorization gate.
  • Review any code that derives trust, access, tenant identity, redirect targets, or SSRF boundaries from request-derived URL values.

What should a good bug bounty report include?

  • The affected Starlette version or dependency evidence from the target.
  • The specific endpoint or behavior that should require authentication or another security condition.
  • A control request with a valid Host showing the expected denial.
  • A malformed Host request showing a different result.
  • Evidence that the protected action, data, or route was actually reached.
  • A clear explanation of whether the edge proxy forwarded the malformed Host or whether forwarded headers were involved.
  • A remediation recommendation: upgrade Starlette, fix path-based middleware, and validate Host at the edge.

Does this matter for AI agents, MCP servers, and LLM APIs?

  • Yes, if those systems use Starlette or FastAPI and expose powerful actions behind Web/API paths.
  • The vulnerability is not model-specific. The model is not the root cause.
  • The risk comes from the control plane around AI systems: tool execution, file access, connector actions, token management, workflow execution, admin APIs, and internal model gateways.
  • Prioritize AI infrastructure that has custom middleware, direct ASGI exposure, weak internal network boundaries, or sensitive admin endpoints.

Closing judgment

CVE-2026-48710 is a small parsing bug with a large design lesson. Security controls should not depend on a URL string reconstructed from client-controlled Host data when routing, proxying, and middleware may interpret request components differently. The fix starts with Starlette 1.0.1, but it should not end there.

Patch the framework. Remove request.url.path authorization gates. Validate Host at the edge. Strip untrusted forwarded headers. Add regression tests for malformed Host values against sensitive paths. Treat AI and agent infrastructure as high-priority if it exposes powerful actions through ASGI services. The most durable remediation is not just blocking one malformed header; it is making sure the security boundary is tied to the route and policy actually being executed.

게시물을 공유하세요:
관련 게시물
ko_KRKorean