
Воспроизводит контрабанду запросов CWE-444 в aiohttp через отклонённые WebSocket-апгрейды, с payload на Python/Rust и Docker-лабораторией, демонстрирующей обход контроля доступа прокси.
Смешивание запросов через отклонённый WebSocket-апгрейд в aiohttp < 3.14.2.
Когда обратный прокси-сервер пробрасывает заголовки Connection: Upgrade + Upgrade: websocket,
уязвимый парсер aiohttp пропускает тело запроса и трактует последующие байты
как конвейерный (pipelined) запрос — обходя контроль доступа на периметре.
Исправлено в 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>
Готовые бинарники будут публиковаться в Releases через release-workflow.
Оба варианта генерируют побайтно идентичные payload'ы (это обеспечивается CI-тестом на идентичность). Если стенд запущен:
python3 poc.py nginx-upgrade 80 backend-vuln
Ожидаемый результат: 1 HTTP-ответ (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 и снова подаются в парсер в finish_response
из web_protocol.py (строка ~771).location /admin { deny all; },
внедрённый /admin всё равно достигает бэкенда, потому что решения
о маршрутизации в Nginx принимаются только на основании внешнего запроса.await request.read() возвращает 0 байт
для апгрейд-запросов в 3.14.1 — тело удерживается ниже уровня обработчика. Примените
патч или удаляйте апгрейд-заголовки на прокси для маршрутов, которые не должны
переключать протокол.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
Сервисы:
| Контейнер | Назначение |
|---|---|
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 | Пробрасывает апгрейд-заголовки (deny all на /admin) |
nginx-default | Не пробрасывает апгрейд-заголовки (нейтрализует уязвимость) |
nginx-strip | Удаляет Connection "" (нейтрализует уязвимость) |
attacker | Rust-бинари: reproduce, fase2, poc |
Полные результаты фаз 1-3 — в findings/.
Connection: close или удаляют заголовки Connection/Upgrade,
не уязвимы. Конфигурация, в которой проявляется расщепление, — это канонический
фрагмент map для WebSocket из документации самого Nginx по проксированию./ws. Доказательства смешивания —
в логах бэкенда, а не в ответе атакующему: в такой топологии это слепой,
однонаправленный примитив. Другие топологии прокси не тестировались.Transfer-Encoding: chunked НЕ смешивает запросы — строка с размером чанка
(например, 3e) попадает в парсер как недопустимый метод, и соединение обрывается.
Через Nginx это работает, но через нормализацию: Nginx снимает chunked-кодирование
с тела и передаёт синтезированный 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 против байт из
request.read()): срабатывает на 3.14.1, молчит на 3.14.2. Обнаруживает,
но не устраняет. Ограничьте область его применения (маршруты-кандидаты на
апгрейд, небольшие тела, статусы, отличные от 101) — наивная версия
буферизует каждое тело в памяти.reqlen на периметре над базовым уровнем заголовков на WebSocket-эндпоинтах — само по себе
низкая достоверность (cookies/JWT/tracing-заголовки шумят), но единственный
сигнал на периметре, когда клиент не отправляет Content-Length
(chunked-трафик на входе).