Segurança · Node.js · Gestão de Vulnerabilidades
RCE do Brotli no Node.js: a checagem que não prova nada
O Node.js corrigiu um RCE crítico no Brotli em 21 de outubro. Rodamos os dois builds oficiais: process.versions.brotli devolve 1.2.0 nos dois.

No dia 21 de outubro de 2026 o Node.js publicou releases de segurança para uma escrita fora dos limites no decodificador Brotli que ele embute. Dados comprimidos malformados conseguem corromper a memória do processo e, no pior caso, executar código do atacante. O GitHub Advisory Database e o NVD dão nota 9.2 — Crítica. O aviso do próprio Node.js classifica como alta e diz que afeta as linhas 26.x, 24.x e 22.x.
Em menos de 24 horas uma instrução estava em todo lugar: rode node -p "process.versions.brotli" e compare a resposta com a faixa afetada.
Nós rodamos nos binários oficiais. O Node 24.22.0, vulnerável, imprime 1.2.0. O Node 24.22.1, que já contém a correção, imprime 1.2.0. O número que todo mundo foi orientado a ler é idêntico nos dois.
Ou seja: a parte difícil dessa vulnerabilidade não é corrigir. A correção é um bump de versão que seu time provavelmente já mergeou. A parte difícil é provar que ela chegou em todo runtime que você opera — e isso é um problema de inventário, não uma consulta de scanner.
Quem vende software para cliente enterprise conhece bem o momento em que isso vira custo: o questionário de segurança chega perguntando se a CVE foi corrigida, com prazo de resposta, e a evidência que você tem em mãos é um número que não distingue corrigido de vulnerável.
O que nós verificamos e o que é alguém falando de si mesmo
Baixamos o node-v24.22.0-linux-x64.tar.xz do nodejs.org, conferimos o SHA-256 contra o SHASUMS256.txt do próprio projeto e rodamos lado a lado com o 24.22.1. Lemos o version.h embutido em seis tags de release. Lemos os avisos na fonte. Esses resultados são nossos e você reproduz em dez minutos.
Outras afirmações muito repetidas nesta divulgação não são nossas e não foram confirmadas de forma independente:
- A Imperva diz ter conseguido execução remota de código contra uma imagem oficial e não modificada do Node.js 24.22.0. É a pesquisa do próprio fornecedor, o exploit não foi publicado, e a própria Imperva afirma que a prova não funcionou em todas as tentativas.
- Vercel e Imperva dizem cada uma ter subido regras de WAF no dia da divulgação. Cada uma é a afirmação de uma plataforma sobre si mesma.
- O site que concentrou a divulgação afirma que Dependabot, pip-audit e Safety não sinalizaram de forma confiável os pacotes PyPI afetados, e que a prova de conceito pública só chega a execução de código em um build deliberadamente enfraquecido. Plausível, coerente com a estrutura do aviso que verificamos, mas não confirmado.
- "Nenhum ataque observado em produção" é uma ausência relatada por trackers. Ninguém consegue confirmar uma ausência.
Leia a primeira lista como fato e a segunda como depoimento. O argumento abaixo só precisa da primeira.
O bug: um retorno que ninguém checou

Nas palavras do próprio Node.js: uma troca de bloco inválida foi reportada como necessidade temporária de mais dados em vez de erro permanente de formato, então o decodificador em streaming continuou anexando bytes de entrada em um buffer interno de oito bytes.
É uma das falhas mais antigas em C. A função devolvia um booleano simples onde precisava de três estados — tudo certo, preciso de mais dados, quebrado em definitivo. Quem chamou leu "preciso de mais dados" e seguiu alimentando o buffer. Um retorno não verificado virou uma escrita fora dos limites.
E a exposição não é opcional. O fetch() nativo do Node negocia e decodifica respostas Brotli sozinho. Uma aplicação fica exposta só por chamar uma URL que não controla — uma API parceira, um webhook, um host de imagens, qualquer requisição que sai do seu servidor.
A string de versão não separa corrigido de quebrado
O Node.js não usa o Brotli do sistema. Ele carrega uma cópia própria da biblioteca em C dentro de deps/brotli. A correção de outubro entrou nessa cópia por cherry-pick, sem subir o número de versão da biblioteca.

Essa é a release corrigida. As macros de versão dizem 1, 2, 0, e o último commit que tocou o arquivo tem nove meses.
Veja o que a versão embutida reporta nas três linhas suportadas. Rodamos o par 24.x como binário e lemos o version.h nas outras quatro tags:
| Release do Node.js | Contém a correção | process.versions.brotli |
|---|---|---|
| 22.23.3 | Não | 1.1.0 |
| 22.23.4 | Sim | 1.1.0 |
| 24.22.0 | Não | 1.2.0 |
| 24.22.1 | Sim | 1.2.0 |
| 26.12.0 | Não | 1.2.0 |
| 26.12.1 | Sim | 1.2.0 |
A faixa afetada da biblioteca no aviso é >= 1.0.3, < 1.2.1, corrigida upstream em 1.0.10, 1.1.1 ou 1.2.1. Toda linha dessa tabela cai dentro da faixa afetada, inclusive as três corrigidas. Uma frota Node 22 totalmente corrigida reporta 1.1.0 — versão que não é corrigida em lugar nenhum upstream e nunca será, porque o Node aplicou a correção sem puxar a release de cima.
A string de versão não é apenas inútil. Ela está errada na direção que assusta.
O teste que funciona
A correção adiciona um novo código de erro do decodificador, -17, para o erro de formato que antes era reportado errado. O Node registra esse código como constante do zlib. A presença dele é, portanto, um marcador confiável da correção no runtime:
node -p "'BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH' in require('node:zlib').constants"
# false -> este build está vulnerável
# true -> este build tem a correção
Rodamos nos dois binários oficiais: false no 24.22.0, true no 24.22.1. Até onde apuramos, ninguém na cobertura publicou um teste de runtime — a orientação disponível para em ler a versão do Node.js ou cruzar faixas de versão em um inventário que você já deveria ter.
Esse one-liner é o que vale levar deste post. Funciona dentro de container, em um passo de CI, por SSH naquela máquina que ninguém lembra de ter provisionado.
Por que seu scanner não tem o que casar
Dois motivos estruturais, ambos visíveis na própria página do aviso.

Primeiro: os únicos pacotes legíveis por máquina anexados a esse aviso Crítico são dois pacotes Python. Não existe entrada npm nem entrada de runtime, porque o código vulnerável é compilado dentro do binário do Node, não instalado de um registro. Uma varredura de dependências de um projeto Node.js não tem nada para apontar. Nenhum lockfile do seu repositório menciona Brotli, e nenhum npm audit vai mencionar.

Segundo: o registro do GitHub não carrega identificador de CVE. O campo diz "No known CVE" enquanto as referências do próprio aviso apontam para a página do NVD da CVE-2026-111497. Consulte a API de advisories do GitHub por essa CVE e você recebe um array vazio. Se a sua ferramenta parte de uma CVE para a base do GitHub — e muita ferramenta faz isso — essa vulnerabilidade não existe.
O que isso custa de verdade
Três linhas de custo, nenhuma delas curiosidade de engenharia.
A evidência de patch vira auditoria com gente alocada. Cada imagem de container, cada imagem base herdada, runners de CI, runtimes serverless, aplicações de executável único, notebooks de desenvolvedor, aquela instância rodando um cron. Nenhum manifesto nomeia o código vulnerável, então a lista precisa sair de um inventário de runtime. Ou você tem esse inventário, ou vai construí-lo nesta semana, com prazo, enquanto o questionário do cliente espera resposta.
Seu scanner vai gritar lobo numa frota já corrigida. O casamento de CVE via SBOM lê a versão da biblioteca embutida e marca um parque Node 22 totalmente corrigido como Crítico, indefinidamente. Um time que aprende a ignorar esse alerta quebrou o alerta — e o próximo. Essa é a parte cara, e ela sobrevive à vulnerabilidade.
No Node 20, 23 ou 25 não existe correção. Essas linhas chegaram ao fim de vida antes da divulgação — 23.x em junho de 2025, 20.x em abril de 2026, 25.x em junho de 2026. Só 22.x, 24.x e 26.x receberam o patch. Para esses times, atualizar de major deixa de ser item de roadmap e vira incidente, o que é uma conversa bem diferente sobre cronograma e orçamento.
Esta é a camada que fica sem dono. Já escrevemos sobre uma zona de disponibilidade derrubando uma fileira de bancos e sobre conter código que você não escreveu; aqui é a mesma falha em tom mais baixo. O time acompanha suas dependências com cuidado e trata o runtime embaixo como constante. Não é constante. É uma biblioteca em C que alguém embutiu, com um número de versão que não significa o que parece significar.
Pontos principais
process.versions.brotlidevolve1.2.0tanto no Node 24.22.0 (vulnerável) quanto no 24.22.1 (corrigido). Não serve para verificar seu patch.- O Node 22.23.4 está totalmente corrigido e reporta Brotli
1.1.0, dentro da faixa afetada do aviso. O casamento por SBOM vai marcá-lo como Crítico para sempre. - O teste confiável de runtime é a presença de
BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCHemrequire('node:zlib').constants. - Não existe aviso npm nem entrada em lockfile, e o registro do GitHub não tem CVE anexada. A varredura automática não vai fazer isso por você.
- As versões corrigidas são 22.23.4, 24.22.1 e 26.12.1. Node 20, 23 e 25 estão em fim de vida, sem patch.
Perguntas frequentes
Estou exposto se minha aplicação nunca chama zlib.brotliDecompress diretamente?
Provavelmente sim. O fetch() nativo do Node decodifica respostas Brotli sozinho, então qualquer requisição do servidor para uma URL que você não controla alcança o decodificador. Proxies reversos e tratamento de corpo de requisição também.
Meu npm audit está limpo. Isso diz alguma coisa aqui?
Não. O código vulnerável é compilado dentro do binário do Node.js e não aparece em nenhum manifesto de pacote. Audit limpo é o resultado esperado em um runtime vulnerável.
Qual a forma mais rápida de checar uma imagem de container?
Rode a checagem de constante contra o interpretador da própria imagem: docker run --rm --entrypoint node <imagem> -p "'BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH' in require('node:zlib').constants". Tudo que imprimir false precisa ser reconstruído sobre uma base corrigida.
Estamos no Node 20. O que fazer? Planejar migração, não patch. O Node 20 chegou ao fim de vida em 30 de abril de 2026 e não vai receber esta correção nem a próxima. Trate como incidente com data marcada.
Levar software feito com IA e produto em estágio inicial até produção significa assumir as camadas que o framework esconde, inclusive as que não têm manifesto de pacote. É o trabalho que a gente faz.