Przejdź do treści

http2shell: podatność RCE w brotli. Sprawdź, czy twoja aplikacja dekompresuje treść

Aktualizacja: 25 października 2026

Okładka artykułu o podatności http2shell (CVE-2026-111497): CVSS 9,2 w dekoderze brotli, wydania naprawcze 21 października, zero potwierdzonej eksploatacji wg CISA

27 lutego 2026 Evgenii Kliuchnikov włożył do repozytorium brotli, kompresji od Google, commit o tytule „Detect and report invalid stream earlier”. Nikt nie nazwał go wtedy poprawką bezpieczeństwa i nie wyszedł z żadnym wydaniem, bo biblioteka nie miała nowej wersji od 27 października 2025. Dziś ten sam commit ma numer CVE-2026-111497, ocenę CVSS v4.0 9,2 i własną nazwę. Najpierw media i badacze mówili o node2shell, bo 21 października Node.js wydał awaryjne wersje 22.23.4, 24.22.1 i 26.12.1. Dzień później naprawione pakiety trafiły do PyPI jako Brotli 1.2.1 i brotlicffi 1.2.1.0. A w piątek 23 października firma Aikido Security opublikowała wpis, z którego wynika, że problem nie kończy się na Node i Pythonie: dekompresujące aplikacje w wielu językach i produktach (między innymi Bun, Envoy i Istio) korzystają z tego samego wadliwego kodu. Stąd nowa nazwa, http2shell.

Zanim zaczniesz panikować, jedno rozróżnienie. Zagrożone jest oprogramowanie, które dekompresuje dane w formacie brotli pochodzące z sieci: klienci HTTP pobierający odpowiedzi z nagłówkiem Content-Encoding: br oraz serwery i frameworki przyjmujące żądania ze skompresowanym ciałem. Serwer, który tylko kompresuje swoje odpowiedzi, nie dotyka wadliwej ścieżki. Jak ujął to użytkownik Hacker News cpburns2009: „Only decompression with Google’s brotli library is affected”. Standardowy mod_brotli Apache’a i nginx nie dekompresują przychodzącego ruchu, więc sama obecność brotli na serwerze www niczego nie przesądza.

Najważniejsze informacje

CoSzczegóły
CVECVE-2026-111497, CVSS v4.0 9,2 (krytyczna), CWE-787 out-of-bounds write i CWE-252 unchecked return value
Co podatnedekoder brotli Google w wersjach 1.0.3 do 1.2.0, plik c/dec/decode.c
Naprawiony upstream1.0.10, 1.1.1, 1.2.1, wydane 21.10.2026; poprawka w kodzie istniała od 27.02.2026
Node.js22.23.4, 24.22.1, 26.12.1 (security release 21.10.2026)
PythonBrotli 1.2.1 i brotlicffi 1.2.1.0 na PyPI (22.10.2026); podatne 1.2.0 i starsze
Kto realnie ryzykujeaplikacje dekompresujące br: wbudowany fetch i node:zlib w Node, urllib3 z requests, httpx, aiohttp i Scrapy w Pythonie
Kto nie ryzykujeserwery tylko kompresujące odpowiedzi; Apache i nginx domyślnie nie dekompresują
Eksploatacjabrak potwierdzonych ataków wg CISA na 23.10; mitigacja: usunięcie Brotli i brotlicffi oraz filtrowanie br na wejściu

Co dokładnie jest zepsute w dekoderze

Brotli podczas dekompresji przełącza wewnątrz strumienia bloki, dla których używa różnych drzew Huffmana. Funkcja DecodeBlockTypeAndLength() obsługuje te przełączenia i w podatnych wersjach zwracała prostą wartość logiczną. Fałsz mógł oznaczać dwie rzeczy: strumień jest uszkodzony albo potrzeba więcej danych. W scenariuszu z opublikowanych opisów dekoder potraktował nieprawidłowy przełącznik bloku jak zapotrzebowanie na więcej danych i dalej dokładał bajty z wejścia do wewnętrznego bufora wielkości ośmiu bajtów. Tak opisał to zespół Node.js: „An invalid block switch was reported as a temporary need for more input instead of a permanent format error, so the streaming decoder kept appending input bytes to an eight-byte internal buffer”. Efekt to zapis poza buforem, a z niego według rekordu CVE wyciek pamięci procesu, awaria albo wykonanie dowolnego kodu.

Poprawka nadaje funkcji pełny kod błędu. Pojawia się nowy status BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH o numerze -17, odróżniony od BROTLI_DECODER_NEEDS_MORE_INPUT, i dekodowanie zatrzymuje się na błędzie formatu w obu ścieżkach, szybkiej i bezpiecznej. Należąca do Aikido strona Open Source Malware zbudowała obie wersje z AddressSanitizer i przepuściła spreparowany strumień: wersja 1.2.0 połykała strumień rzędu 23 MB i uparcie prosiła o więcej, wersja 1.2.1 odrzucała go za każdym razem z błędem -17. To z raportu Aikido, nie moje własne badanie, ale opis diffu sprawdziłem sam na commicie b418fa8 i się zgadza.

Fix z lutego, który przez osiem miesięcy nie był wydaniem

Tu jest właściwa historia. Źródłowa zmiana 0ed4268 z lutego naprawiała zachowanie dekodera, ale nikt jej nie skojarzył z luką bezpieczeństwa i nie doczekała się wydania, bo ostatnia wersja brotli, 1.2.0, wyszła 27 października 2025, przed poprawką. W lutym nikt nie wiedział, że to w ogóle luka. Wnioski na Hacker News były bezlitosne. Użytkownik mirashii napisał, że „the brotli library saw no releases for almost a full year”, Google buduje sobie wewnętrznie z najnowszego kodu, więc sam nie był zagrożony, a wydania open source stały się w tym projekcie raczej dodatkiem niż obowiązkiem. Dodał, że ponad sto commitów z różnymi poprawkami czekało, aż incydent bezpieczeństwa zmusi do wydania.

Sporo wyjaśnia stanowisko OpenAI z piątku. Oficjalne konto napisało, że luka wyszła na jaw podczas ćwiczenia z red teamingu i że OpenAI znalazło ją już w lutym: zdalne wykonanie kodu bez uwierzytelnienia, działające w obu kierunkach, czyli złośliwy klient przejmuje niezałatany serwer i złośliwy serwer przejmuje niezałatanego klienta. Advisory OpenAI prowadzi na stronie patchtheplanet.org/brotli, a blog Node.js dziękuje za zgłoszenie zespołowi OpenAI Codex Security, konkretnie Khai Tran i Sylvie Mayer. Użytkownik HN cowlevel podsumował to krótko: poprawka z lutego „wasn’t recognized as security relevant so wasn’t important enough to make a release”, dopóki agent OpenAI tego nie wychwycił. Kompletnej strony advisory nie odczytam, bo patchtheplanet.org buduje treść w przeglądarce, więc opieram się na tweecie i streszczeniu Aikido.

Formalnie wszystko ruszyło w jednym tygodniu. W zapowiedzi na liście oss-security Node.js obiecał wydania linii 22, 24 i 26 „on or shortly after 2026-10-21”, z najwyższą wagą high. 21 października projekt brotli opublikował w końcu trzy wersje z notką SECURITY i numerem CVE, a commit z lutego trafił do gałęzi wydawniczych jako cherry-pick b418fa8. 22 października na PyPI wylądowały naprawione pakiety, brotli 1.2.1 o 13:57 UTC i brotlicffi 1.2.1.0 o 16:19 UTC. W piątek 23 października Aikido nadało sprawie nazwę http2shell, potwierdziło podatność kolejnych produktów i podało listę napraw: Bun 1.4.4, Envoy 1.40.0, 1.39.3, 1.38.6, 1.37.8, Istio 1.29.9, 1.30.6, 1.31.2.

Kto realnie ryzykuje

Dwie drogi wejścia. Pierwsza: twoja aplikacja sama odpytuje serwery i dekompresuje odpowiedzi, na przykład webhooki, pobieranie podglądu URL, crawler czy funkcja „pobierz ten adres”. Wtedy złośliwy serwer (albo każdy serwer, który sam został przejęty) może wysłać odpowiedź z Content-Encoding: br i trafić do dekodera. Druga: aplikacja przyjmuje żądania ze skompresowanym ciałem i dekompresuje je przed obsłużeniem. Aikido opisuje to jako wariant przypadku Express 5 po stronie Node. Złośliwy klient potrzebuje tylko jednego żądania, bez konta i bez uwierzytelnienia, warunkiem jest wyłącznie włączone dekompresowanie po twojej stronie.

W Pythonie dekodowanie często dzieje się bez twojej wiedzy. Pakiety Brotli i brotlicffi kompilują ten sam kod dekodera co Node.js, a biblioteki urllib3 (a więc i requests), httpx z dodatkowym extras o nazwie brotli, aiohttp oraz Scrapy używają ich automatycznie, jeśli tylko są zainstalowane w środowisku. Importu w twoim kodzie nie musi być. Brotli pobrało 82,8 mln razy w ostatnim miesiącu, w tym 4,2 mln w ciągu doby, wg pypistats na 24 października. Po stronie Node dekompresuje wbudowany fetch() i oprogramowanie typu body-parser przez node:zlib. Jest i haczyk w drugą stronę: jak zauważył na HN użytkownik thayne, w fetchu Node.js nie ma opcji wyłączenia przyjmowania br.

Część stosu jest poza zasięgiem. Jak oceniał w dyskusji na HN użytkownik doodlesdev, rustowy crate brotli to port, nie wiązanie do kodu C, więc ta konkretna wada raczej go nie dotyczy, choć octoberfranklin z HN przypomina, że wystarczy binary linkowany z systemowym libcurl z brotli, by sprawa wróciła, i podaje sprawdzian przez ldd. Użytkownik ataraxia937 sprawdził ponad dwieście repozytoriów swojej organizacji w Node, Pythonie i C# i nie znalazł ani jednego zagrożonego, bo żaden projekt sam nie wywołuje dekodera; w tym samym oddechu przypomina, że moduł Brotli w .NET to wiązanie do wadliwej implementacji Google.

Jak sprawdzić i zabezpieczyć

Zacznij od Pythona, bo tam zależność przechodzi przez słynne extras. W każdym środowisku:

pip list | grep -i brotli
pip show Brotli brotlicffi

Jeśli masz starsze niż 1.2.1 i 1.2.1.0, aktualizacja wygląda tak:

pip install -U "Brotli>=1.2.1"
pip install -U "brotlicffi>=1.2.1.0"

Potem sprawdź lockfile i drzewo zależności, bo pakiet wraca przez extras innych bibliotek. Aikido podaje gotowe komendy: grep -i brotli poetry.lock uv.lock requirements*.txt oraz pipdeptree -r -p Brotli. Obrazy kontenerów trzeba przebudować, bo obraz zbudowany na podatnej wersji pozostaje podatny mimo nowego pipa. Per obraz działa docker run --rm obraz pip show Brotli brotlicffi. Na binary systemowych i skompilowanych narzędziach pomaga ldd sciezka/do/programu | grep brotli. W Node aktualizujesz runtime do 22.23.4, 24.22.1 albo 26.12.1, w Buntu do 1.4.4, a w Envoy i Istio do wersji z listy Aikido. curl i libcurl wymagają odświeżenia swojej zależności brotli lub pakietu od dystrybutora.

Jeśli aktualizacja nie jest możliwa od ręki, mitigacja jest dwuwarstwowa. Po pierwsze usuń oba pakiety Pythonowe (pip uninstall Brotli brotlicffi), wtedy urllib3 i requests zostają przy gzip i deflate, przy czym serwer, który i tak wysyła br, może zwrócić błąd albo nieczytelną odpowiedź, automatycznego fallbacku nie ma gwarancji. Po drugie odrzucaj na reverse proxy albo w middleware żądania z Content-Encoding: br, porównując case-insensitive także listy kodowań, i usuwaj br z wychodzących Accept-Encoding. Sama zmiana Accept-Encoding nie wystarczy: serwer i tak może wysłać brotli, a urllib3 wybiera dekoder po nagłówku odpowiedzi, nie po tym, co obiecywał klient. I jedna rzecz, którą musisz wiedzieć na dzień pisania: wg Aikido na 23 października bazy advisory nie mapują CVE do pakietów PyPI, więc pip-audit, Dependabot ani Safety tego nie wyłapią. Sprawdzenie musi być ręczne.

Czy to kolejny Log4Shell

Na razie wygląda na to, że nie. W rekordzie CVE notka SSVC od CISA podaje Exploitation: none, a Aikido cytuje The Hacker News, że na 22 października nie było dowodów na wykorzystanie w naturze. Użytkownik FreakLegion, powołujący się na znajomych z zespołów reakcji (CrowdStrike, Unit 42, Talos), napisał na HN: „There were no known breaches or end-to-end exploits as of Friday” i dodał, że to była przede wszystkim procedurowa porażka, „It wasn’t another Log4Shell”. Ktoś na HN (iracigt) odtworzył awarię jednym httpx.get(URL) w Pythonie, ale nie doszedł do pełnego łańcucha RCE, zatrzymał się na prymitywach odczytu i zapisu.

To klasa podobna do php2shell, o którym pisaliśmy kilka dni temu: zdalne wykonanie kodu przez ruch HTTP, bez logowania, z warunkami wstępnymi po stronie ofiary. Różnica jest taka, że tu na 24 października brak potwierdzonych kompromitacji. Zbliżenie do Log4Shell bierze się z rozmiaru dystrybucji: dekoder brotli jest wbudowany w Node, Bun, Envoy, Istio i setki mniejszych narzędzi, a każda biblioteka go bundlująca musi wydać własną aktualizację, bo upstream nie załatani tego za wszystkich. Skalę problemu podbija też PyPI, gdzie w tym samym miesiącu LiteLLM miało zainfekowane wydanie, o czym też pisaliśmy. Obsługa tej podatności sprowadza się więc do starej rutyny: sprawdź drzewo zależności, podbij wersje, przebuduj obrazy. To samo co zawsze, tylko w bibliotece, o której większość z nas dowiedziała się dopiero z komunikatu o krytycznej luce.

Źródła

Dopisek (25 października): w tej samej fali poprawek, do której należy brotli, siedzi też para luk PHP2Shell w samym silniku PHP. Konto HN cloudlandsdev, za którym stoi badacz James Pavur, wystawiło na celowo podatnym serwerze portfel z Bitcoinem jako test, czy ktoś ma gotowy exploit. Po dobie portfel stoi, a nagroda urosła przez darowizny do ponad 2000 dolarów. Zapis zabawy: Steal My Bitcoin: 2000 dolarów za udane włamanie przez lukę PHP.

Powiązane artykuły