
거부된 WebSocket 업그레이드를 통한 aiohttp CWE-444 요청 스머글링을 재현하며, 프록시 접근 제어 우회를 시연하는 Python/Rust 페이로드 및 Docker 랩을 포함합니다.
aiohttp < 3.14.2에서 거부된 WebSocket 업그레이드를 통한 요청 스머글링.
역방향 프록시가 Connection: Upgrade + Upgrade: websocket
헤더를 전달하면, 취약한 aiohttp 파서는 요청 본문을 건너뛰고
후행 바이트를 파이프라인된 요청으로 해석하여 에지 접근 제어를 우회합니다.
aiohttp 3.14.2에서 수정됨 (커밋 6ae358f).
저자: João Victor Botelho (JV Botelho) — https://glitchedcat.com
Python 버전을 복사하여 붙여넣으세요 (의존성 없음, 표준 라이브러리만 사용):
curl -O https://raw.githubusercontent.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling/main/poc.py
python3 poc.py <proxy-host> <proxy-port> <backend-host>
또는 Rust 버전을 빌드하세요 (의존성 없음, 표준 라이브러리만 사용):
git clone https://github.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling
cd cve-2026-69243-poc-aiohttp-smuggling/poc
cargo build --release
./target/release/cve-2026-69243-poc <proxy-host> <proxy-port> <backend-host>
사전 빌드된 바이너리는 릴리스 워크플로를 통해 릴리스에 게시됩니다.
두 버전 모두 바이트 단위로 동일한 페이로드를 생성합니다 (CI 패리티 테스트로 보장). 랩이 실행 중이면:
python3 poc.py nginx-upgrade 80 backend-vuln
예상 출력: HTTP 응답 1개 (WebSocket upgrade rejected). 그런 다음 확인:
# Backend processed 2 requests (/ws + smuggled /admin):
docker logs backend-vuln | grep -c '"path".*"/admin"'
# Nginx only logged 1 request (the /ws):
docker exec nginx-upgrade cat /logs/nginx-upgrade.access.log | grep -c '/admin'
백엔드 카운트 > 0이고 Nginx 카운트 = 0이면 CWE-444 분할이 확인된 것입니다. 이 PoC는 단일 TCP 세그먼트를 전송합니다. 본문이 곧 스머글링된 요청이며, Nginx는 이를 본문으로 처리하는 반면 aiohttp는 이를 두 번째 요청으로 처리합니다.
_http_parser.pyx는 본문이 소비되기 전에
업그레이드 감지 시 2(본문 건너뛰기)를 반환합니다(약 863행). 본문 바이트는
_message_tail에 남아 web_protocol.py의 finish_response(약 771행)에서
파서로 다시 전달됩니다.location /admin { deny all; }가 있을 때,
스머글링된 /admin은 Nginx 라우팅 결정이 외부 요청에 대해서만
이루어지기 때문에 여전히 백엔드에 도달합니다.await request.read()는 업그레이드
요청에 대해 0바이트를 반환합니다. 본문은 핸들러 계층 아래에서 보류됩니다.
패치를 적용하거나, 프로토콜을 전환해서는 안 되는 경로의 프록시에서
업그레이드 헤더를 제거하세요.git clone https://github.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling
cd cve-2026-69243-poc-aiohttp-smuggling
docker compose up -d
# Run the PoC:
docker compose run --rm --entrypoint /app/poc attacker nginx-upgrade 80 backend-vuln
서비스:
Phase 1-3 전체 조사 결과는 findings/에 있습니다.
Connection: close를
보내거나 Connection/Upgrade 헤더를 제거하는 Nginx 구성은 취약하지
않습니다. 분할을 노출하는 구성은 Nginx 자체 프록시 문서의 표준 WebSocket
map 스니펫입니다./ws 응답만 받습니다. 스머글링의 증거는 공격자 응답이 아닌 백엔드
로그에 있습니다. 이 토폴로지에서는 블라인드 단방향 프리미티브입니다.
다른 프록시 토폴로지는 테스트되지 않았습니다.Transfer-Encoding: chunked는 스머글링하지 않습니다. 원시 청크 크기 라인
(예: 3e)이 유효하지 않은 메서드로 파서에 도달하여 연결이 종료됩니다.
Nginx를 통하면 작동하지만 정규화를 통해서입니다. Nginx가 본문의 청크를
해제하고 생성된 Content-Length를 전달하므로 백엔드는 동일한 CL 경로를 통해
공격받습니다. --chunked 플래그는 프록시 경로를 시연합니다.WebSocketResponse가 아닌 응답을 반환해야 합니다. 라우트에서 WebSocket을
사용하지 않는 대부분의 앱은 기본적으로 거부합니다(프레임워크가 404를 반환
하거나 다음 핸들러로 넘어갑니다).poc/ # Rust cargo project
├── Cargo.toml
├── src/main.rs # CLI binary
├── src/lib.rs # Library + unit tests
├── tests/parity.rs # Cross-language payload parity test
└── fuzz/ # cargo-fuzz targets
poc.py # Python PoC (copy-paste from blog)
attacker/ backend/ frontend/ # Docker lab services
docker-compose.yml # 7-service lab
findings/ # Research notes (Phase 1-3)
.github/workflows/
├── ci.yml # Build, test, clippy, parity, integration, fuzz
└── release.yml # Cross-compile + GitHub Release
전체 분석은 findings/fase3-deteccao.md를 참조하세요. 프로덕션에서 중요한
주의사항을 포함한 요약:
X-Forwarded-For/Proxy Protocol을 정규화하거나 업스트림
연결 ID + 연결별 요청 순서를 로깅해야 합니다. 클라이언트 IP + 시간 창
만으로는 약합니다(NAT, keep-alive, 동시성).content_length vs request.read()의 바이트):
3.14.1에서 발동하고 3.14.2에서는 조용합니다. 탐지는 하지만 완화하지는
않습니다. 범위를 한정하세요(업그레이드 후보 라우트, 작은 본문, 101이 아닌
상태 코드). 단순한 버전은 모든 본문을 메모리에 버퍼링합니다.reqlen — 단독으로는
신뢰도가 낮지만(쿠키/JWT/추적 헤더가 노이즈가 많음), 클라이언트가
Content-Length를 보내지 않을 때(청크 수신) 유일한 에지 신호입니다.| Container | 용도 |
|---|
backend-vuln | aiohttp 3.14.1 (취약), READ_BODY=false |
backend-vuln-read | aiohttp 3.14.1, READ_BODY=true (핸들러가 도움이 될 수 없음을 입증) |
backend-patched | aiohttp 3.14.2 (수정됨) |
nginx-upgrade | 업그레이드 헤더 전달 (/admin에 대해 deny all) |
nginx-default | 업그레이드 전달 없음 (버그 중화) |
nginx-strip | Connection "" 제거 (버그 중화) |
attacker | Rust 바이너리: reproduce, fase2, poc |