Akademia NIS2/KSC2 od sekuraka. 19 października start drugiej edycji!

Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!

 

Node2Shell (CVE-2026-111497): RCE przez dekompresję Brotli w Node.js

23 października 2026, 16:19 | W biegu | 0 komentarzy

Ostatnie dni były szczególnie intensywne dla naszej cyber-niszy. Najpierw zagrożenie ze strony słabych kluczy kryptograficznych pośredniego CA, potem podatność dotykająca szerokie spektrum web aplikacji opartych na PHP, podatność w BIND, GitLab. Dzieje się. Niestety LLMy skracają czas powstania exploita i chociaż poszukiwanie podatności nadal wymaga umiejętności i głębokiej wiedzy technicznej, to dzieci skryptu i inni cyberprzestępcy uzyskują niebezpieczne narzędzia szybciej (lub zyskują zdolności ich uruchamiania). 

Rysunek 1. Schemat pokazujący genezę poda

Wystarczyła jedna odpowiedź HTTP z nagłówkiem Content-Encoding: br lub jedno zapytanie POST zawierające skompresowane przy pomocy Brotlii, żeby wykonać kod atakującego w procesie Node.js. Pierwotnie zgłoszony został błąd typu memory corruption (konkretnie out-of-bounds write, buffer overflow), w napisanej w C bibliotece Brotli. Błąd otrzymał identyfikator CVE-2026-111497 i według CVSS 4.0 został wyceniony na 9.2 czyli krytyczny. Jest to opracowany w Google, upubliczniony w 2015 roku (opisany w RFC 7932) algorytm kompresji bezstratnej, który miał zastąpić gzip/zlib. Brotli łączy trzy mechanizmy – LZ77, kodowanie Huffmana oraz statyczny słownik (zoptymalizowany pod HTML/CSS/JS/JSON). Jak to bywa w przypadku mechanizmów kompresji dekoder Brotli to state-machine, który parsuje w pełni kontrolowany przez atakującego bitstream. 

Node.js to popularne środowisko uruchomieniowe JS oparte na silniku V8. Integruje wiele popularnych zależności takich jak OpenSSL, zlib czy właśnie Brotli. W tej technologii powstało wiele backendów webowych, API czy narzędzi CLI. W celu aktualizacji zależności należy wykonać update całego środowiska Node.js, nie wystarczy wykonanie polecenia npm. Podatność dotyka następujących wersji:

Node.js:wersje podatne (sprzed poprawki z 21.10.2026):

  • 26.x przed 26.12.1 → aktualizacja do 26.12.1
  • 24.x przed 24.22.1 → aktualizacja do 24.22.1
  • 22.x przed 22.23.4 → aktualizacja do 22.23.4
  • 25.x, 23.x, 20.x i starsze – wszystkie podatne (EOL, brak poprawki)

Dla samej biblioteki mówimy o wersjach:

  • linia 1.2.x przed 1.2.1 → 1.2.1
  • linia 1.1.x przed 1.1.1 → 1.1.1
  • linia 1.0.x do 1.0.9 włącznie → 1.0.10

Warto pamiętać, że przy pakietach niektórych dystrybucji, linkowana jest wersja systemowa libbrotli a nie Node.js i to właśnie ją należy zaktualizować.

Zapytania kompresowane przez Brotli można rozpoznać przez wartość nagłówka Content-Encoding, która jest ustawiona na wartość br. Przy czym obsługa tego kodowania nie musi odbywać się explicite przez wywołanie jawne zlib.brotliDecompress(), brotliDecompressSync() i strumieniowy createBrotliDecompress(), ale również implicite przez wbudowane fetch(), który automatycznie spróbuje dekompresować spakowane tym formatem dane. Oprócz tego podatność może wystąpić przez biblioteki jak chociażby Express 5, klienty HTTP, proxy. Czyli programista aplikacji nie musi świadomie wywoływać (chociaż czy w dobie LLM generated kodu ktokolwiek ma jeszcze świadomość co się dzieje?) dekodera – to znacznie poszerza zakres podatnych aplikacji, zapytania nie muszą być HTTP – mogą to być też inne metody komunikacji jak chociażby websockety. Warto jednak zauważyć, że “czysty” serwer Node.js utworzony przy pomocy wywołania http.createServer() nie będzie podatny. 

Przyczyną podatności był błąd  – programiści zapomnieli obsłużyć przełączenie bloku (block switch). W Brotli dane wewnątrz meta-bloku są podzielone na trzy niezależne strumienie: literały, komendy (insert-and-copy) i odległości (distance). Każdy z tych strumieni może używać różnych „typów bloków” (block types) – a typ bloku decyduje, który zestaw drzew Huffmana jest użyty do dekodowania w danym momencie. „Przełączenie bloku” to właśnie moment w strumieniu, gdy dekoder przechodzi z jednego typu bloku na inny. Działa to tak: nagłówek deklaruje, ile typów bloków istnieje w danej kategorii (NBLTYPES, od 1 do 256) oraz jak długi jest bieżący blok. Dekoder zlicza przetworzone elementy i gdy licznik długości bloku (block_length) spada do zera, czyta z bitstreamu kod przełączenia: numer nowego typu bloku i jego długość. Potem dekoduje dalej, już używając drzew przypisanych nowemu typowi.

I teraz sedno podatności: jeśli NBLTYPES == 1, w danej kategorii istnieje tylko jeden typ bloku – nie ma na co się przełączać. Poprawnie zbudowany strumień nigdy nie wygeneruje w takim przypadku kodu przełączenia; format ustawia wtedy długość bloku na wartość praktycznie nieosiągalną (2²⁴), żeby licznik nigdy nie doszedł do zera. Dotarcie do przełączenia bloku przy jednym typie to więc stan, który może powstać tylko w celowo spreparowanym, nieprawidłowym strumieniu.

Prawidłowa implementacja dekodera powinna odrzucić tak źle przygotowane dane. Jednak podatny kod pozwalał na przetwarzanie danych dalej, zamiast przerwać przetwarzanie natychmiast. Wprowadzone poprawki zmieniły zachowanie biblioteki w taki sposób, że rzuca ona wyjątek BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH. 

static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength(
    int safe, BrotliDecoderState* s, int tree_type) {
  brotli_reg_t max_block_type = s->num_block_types[tree_type];
  const HuffmanCode* type_tree = &s->block_type_trees[
      tree_type * BROTLI_HUFFMAN_MAX_SIZE_258];
  const HuffmanCode* len_tree = &s->block_len_trees[
      tree_type * BROTLI_HUFFMAN_MAX_SIZE_26];
  BrotliBitReader* br = &s->br;
  brotli_reg_t* ringbuffer = &s->block_type_rb[tree_type * 2];
  brotli_reg_t block_type;

  /* VULNERABILITY: a block switch is impossible when only one block type was
     declared. The decoder returns BROTLI_FALSE, which the fast path ignores
     (void wrappers) and the safe path reads as NEEDS_MORE_INPUT, so decoding
     continues with inconsistent state instead of failing on a format error. */
  if (max_block_type <= 1) {
    return BROTLI_FALSE;
  }

  /* Read 0..15 + 3..39 bits. */
  if (safe) {
    BrotliBitReaderState memento;
    BrotliBitReaderSaveState(br, &memento);
    if (!SafeReadSymbol(type_tree, br, &block_type)) return BROTLI_FALSE;
    if (!SafeReadBlockLength(s, &s->block_length[tree_type], len_tree, br)) {
      s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE;
      BrotliBitReaderRestoreState(br, &memento);
      return BROTLI_FALSE;
    }
  }
  /* ... block_type decoding / ring-buffer update ... */
  ringbuffer[0] = ringbuffer[1];
  ringbuffer[1] = block_type;
  return BROTLI_TRUE;
}

Listing 1. Podatny element kodu z biblioteki brotli c/dec/decode.c (przed commitem 0ed4268a wprowadzającym poprawkę)

Początkowo zgłaszający (Matteo Collina) określił błąd jako memory leak, pierwszy exploit pokazał, że efektem podatności jest memory leak i DoS, jednak Imperva Red Team udowodnił, że wpływ może być wyższy i zaprezentował działającego exploita prowadzącego do RCE na domyślnych obrazach Node.js dla architektury arm64. Exploit działa niedeterministycznie, ale to wystarczający dowód, że należy się patchować. 

Node2Shell (CVE-2026-111497) pokazuje, jak jeden pominięty warunek poprawności formatu w natywnej zależności zamienia rutynową dekompresję Brotli w zdalne wykonanie kodu – a ponieważ podatny jest każdy, kto dekompresuje, niezależnie od strony połączenia, jedyną pełną ochroną jest natychmiastowa aktualizacja Node.js.

~BLH

Aktualizacja 23.10.2026 17:15

Możliwe jest fingerprintowanie podatnych serwerów: https://x.com/julianor/status/2113295105289310528

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

Odpowiedz