Principais conclusões
- Uma dependência transitiva é um pacote que você instala porque algo do qual você depende precisa dele.
- Um pacote do npm pode trazer centenas de dependências transitivas, cada uma delas capaz de executar código com o seu acesso.
- Outros mantenedores definem os intervalos em níveis mais profundos da sua árvore, de modo que as correções e os ataques chegam de acordo com o cronograma deles.
- A proteção precisa funcionar no nível do registro para abranger pacotes que sua equipe nunca revisou.
Experimente isso em qualquer projeto no qual você esteja trabalhando há mais de alguns meses. Abrir package.json e conte as dependências. Em seguida, execute npm ls --all e veja o quanto passa pela tela.
A lista em package.json geralmente fica na casa das dezenas. A árvore é bem mais longa, mesmo com o npm marcando as entradas repetidas como deduplicadas. Tudo nessa árvore que você não listou pessoalmente é uma dependência transitiva, e é a parte da sua pilha que recebe menos atenção, embora realize uma grande parte do trabalho.
O que é, na verdade, uma dependência transitiva
Uma dependência direta é aquela que você solicitou. Você executou npm install express, ele caiu em package.json, e alguém da sua equipe tomou uma decisão a respeito.
Uma dependência transitiva é aquela que você não solicitou. O Express precisa de seus próprios pacotes, esses pacotes precisam dos deles, e o npm resolve toda a cadeia para você. Nada disso aparece no seu projeto package.json, mas tudo isso fica registrado no seu arquivo de bloqueio, e tudo o que for realmente instalado vai para node_modules. Qualquer parte do código que seja executada, seja durante a instalação ou quando seu aplicativo a carregar, terá o mesmo nível de acesso que o processo que a está executando. No seu laptop, isso significa sua conta de usuário; e no CI, significa o executador e todos os segredos aos quais ele tem acesso.
É útil dar um nome a isso, então vamos chamá-lo de “risco herdado”. O risco herdado é a exposição que seu projeto assume por causa dos pacotes que vieram junto com aqueles que você escolheu. Você não os avaliou, não escolheu suas versões e, na maioria dos casos, nem sabia quais eram. Mesmo assim, você os distribui.
Por que a árvore fica tão grande?
Parte disso se deve simplesmente à forma como o ecossistema do JavaScript se desenvolveu. O npm facilitou a publicação de pequenos pacotes com uma única finalidade, e a cultura se adaptou a isso. Um utilitário que faz bem uma única coisa acaba sendo incorporado a centenas de outros pacotes, e cada um deles traz consigo seus próprios pequenos auxiliares. Como explica a página do SecureChain para npm da TuxCare, um pacote npm de nível superior pode incluir centenas de dependências transitivas.
A outra parte é a forma como o npm se organiza node_modules. Sempre que possível, o npm os guindastes içam os pacotes até o topo da árvore por isso, a maioria fica lado a lado na parte superior. Isso é ótimo em termos de espaço em disco, mas péssimo para a intuitividade. Percorra a tela node_modules e nada indica que um pacote está em uma profundidade de quatro camadas e só está lá porque algo de que você nunca ouviu falar o solicitou.
Se você quiser ver as camadas, npm explain <package> vai mostrar todos os caminhos que levam a um determinado pacote. Execute o comando em algum item aleatório do seu arquivo de bloqueio e não se surpreenda se mais de uma cadeia levar até ele.
Você não é o proprietário dos intervalos de versão
Essa é a parte que confunde as pessoas. Ao adicionar uma dependência, você escolhe o intervalo de versões dela. Quando essa dependência adiciona o seu próprio intervalo, o mantenedor é quem o escolhe, e você o herda.
A maioria desses intervalos usa um símbolo de seta para cima, como ^4.17.0, que O semver trata como qualquer versão 4.x a partir da 4.17.0. Sua arquivo de bloqueio indica as versões exatas que funcionaram da última vez; é por isso que é preciso fazer o commit e usar npm ci no CI é extremamente importante. Mas, no momento em que alguém exclui e regenera o arquivo de bloqueio, executa uma atualização ou instala em um projeto que não o possui, os intervalos na árvore de código podem ser resolvidos para a versão mais recente permitida, incluindo aquela publicada há uma hora.
Isso é prático quando um mantenedor lança uma correção. Mas vira um problema quando ele lança outra coisa.
Os invasores descobriram isso há muito tempo. Já em 2018, um novo mantenedor do popular event-stream pacote adicionou uma dependência chamada flatmap-stream, que acabou por conter código destinado a roubar fundos de um aplicativo específico de carteira de Bitcoin. Ninguém que usasse event-stream tinha escolhido flatmap-stream. Acabou de chegar.
A mesma tática se repetiu em junho de 2026. Quando os invasores obtiveram acesso de publicação ao escopo npm do Mastra, eles enviaram mais de 140 versões de pacotes comprometidas em menos de duas horas, segundo reportagens do The Hacker News e Soquete. O código do Mastra em si não sofreu alterações. O que mudou foi a lista de dependências, que agora inclui easy-day-js, um sósia do dayjs biblioteca de datas. Quem quer que controlasse easy-day-js havia publicado uma versão 1.11.21 limpa e funcional no dia anterior e, em seguida, lançou a versão 1.11.22 com um gancho pós-instalação malicioso no início do dia 17 de junho, pouco antes do lançamento das versões do Mastra. As novas versões do Mastra solicitavam ^1.11.21, de modo que toda nova instalação direcionava diretamente para a versão corrompida. Se você tivesse analisado uma dessas versões, a única mudança real que teria encontrado em package.json havia apenas uma nova dependência. Tudo o que era perigoso ficava um nível abaixo.
Por que a correção de uma vulnerabilidade grave é tão demorada
As vulnerabilidades conhecidas também se acumulam nas camadas mais profundas da árvore. Quando a Endor Labs analisou pacotes Java amplamente utilizados para seu relatório “State of Dependency Management” de 2022, constatou que cerca de 95% das dependências vulneráveis eram transitivas. Trata-se de pacotes Maven; portanto, considere esse número como uma referência de escala para o npm, e não como um dado específico do npm.
Digamos que um CVE apareça em um pacote três níveis abaixo de qualquer um que você tenha escolhido. A correção é lançada no upstream e, em um mundo ideal, sua árvore simplesmente a incorpora. Na prática, isso só funciona se todos os pacotes entre você e a correção permitirem a nova versão. Se qualquer pacote pai na cadeia tiver fixado uma versão exata ou limitado seu intervalo abaixo da versão corrigida, a resolução normal para por aí. Agora você fica esperando que aquele mantenedor lance uma versão, e possivelmente o mantenedor acima dele também.
É por isso que npm audit pode ser muito frustrante. É útil para indicar que há algo errado nas camadas mais profundas da árvore. Mas é muito menos eficaz na hora de resolver o problema, porque, muitas vezes, não cabe a você fazer a correção. npm audit fix não aplicará uma mudança de versão principal do Semver sem --force, e --force é uma maneira bastante direta de descobrir quais partes do seu aplicativo apresentam falhas.
Além disso, há substituições, que O npm foi adicionado na versão 8.3. Uma substituição permite forçar o uso de uma versão específica de um pacote transitivo ou substituí-lo por um fork, independentemente do que o pacote pai tenha solicitado. Isso pode eliminar a vulnerabilidade. Apenas deixe claro o que você fez. Agora você está executando uma combinação de pacotes que o mantenedor do pacote pai provavelmente nunca testou, e a substituição tende a permanecer no seu package.json muito tempo depois que ninguém mais se lembrar por que está lá.
O mantenedor que não vai mais voltar
Tudo isso pressupõe que alguém na origem ainda esteja lançando novas versões. Nas camadas mais profundas de uma árvore de código grande, nem sempre é esse o caso.
Muitos dos pacotes na parte inferior de uma árvore são pequenos e estão praticamente concluídos, e alguns de seus mantenedores já deixaram o projeto há anos. Quando um CVE é identificado em um desses pacotes, não há ninguém para implementar a correção, portanto não há nada para ser propagado para os níveis superiores. O scanner continua sinalizando o problema, e não há uma correção no upstream para que uma substituição possa ser direcionada. O que resta é criar um fork e aplicar o patch por conta própria, encontrar alguém que faça o backport de uma correção ou substituir qualquer pacote de nível superior que dependa dele. Nenhuma dessas opções é rápida, e a última pode se transformar em uma reescrita para a qual ninguém tem tempo.
A rota de retroportagem é a razão de ser do Suporte de Ciclo de Vida Infinito (ELS) da TuxCare. Para os pacotes abrangidos por esse serviço, o ELS retroporta correções de segurança para as versões exatas que você já está executando, mesmo após o término da manutenção do projeto original, para que você possa permanecer na versão atual, em vez de ser forçado a fazer uma atualização.
Protegendo a árvore inteira
A maioria das correções acima é feita dentro do seu projeto, um pacote por vez, e geralmente depois que o problema já foi incorporado à sua árvore de código. Sobrescritas, correções de auditoria e fixações manuais são formas de corrigir algo que chegou do upstream.
A alternativa é mudar a origem de toda a árvore de dependências. Essa é a ideia por trás do SecureChain. Ele substitui o registro público do npm por um repositório selecionado a partir do qual suas instalações são resolvidas, incluindo pacotes transitivos. Os pacotes são recompilados a partir de código-fonte verificado em ambientes fortificados e isolados e analisados em busca de malware conhecido e padrões de código suspeitos; qualquer item que não passe na inspeção é bloqueado antes de chegar ao seu ambiente. Se algo de que sua árvore de dependências precise ainda não estiver no catálogo, a TuxCare o adiciona. Os pacotes ativos recebem patches de acordo com o SLA à medida que novos CVEs são divulgados e, quando um pacote mantido pelo SecureChain chega ao fim de vida útil, o ELS dá continuidade à cobertura sem que seja necessária nenhuma ação de sua parte.
Depois que você configurar o npm ou seu gerenciador de repositórios para ele, o trabalho do dia a dia não muda. O npm, o Yarn e o pnpm mantêm os mesmos comandos, e o SecureChain funciona em conjunto com o Artifactory, o Nexus ou o GitHub Packages, caso seja assim que sua equipe já faça o proxy dos pacotes. O que muda é que o pacote quatro níveis abaixo, aquele que ninguém jamais ouviu falar, passa pela mesma recompilação e triagem que a estrutura que você escolheu propositalmente.
Por onde começar
Antes de fazer qualquer alteração, tenha uma visão clara do que você está executando. Execute npm ls --all e salve o resultado para poder compará-lo mais tarde. Escolha alguns pacotes que você não reconheça e execute npm explain neles. Certifique-se de que seu arquivo de bloqueio tenha sido confirmado e que o CI utilize npm ci, de modo que as compilações são instaladas a partir do arquivo de bloqueio, em vez de resolver novamente os intervalos, e as versões só são atualizadas quando alguém faz um commit de um arquivo de bloqueio atualizado.
Em seguida, analise seus últimos relatórios de auditoria e classifique as constatações em duas categorias: as que você escolheu e as que você herdou. A segunda pilha é um bom indicador de onde realmente se situam os riscos herdados, e é essa que mais vale a pena examinar com mais atenção.
Perguntas frequentes
O que é uma dependência transitiva?
Uma dependência transitiva é um pacote que seu projeto instala porque uma de suas dependências precisa dele, e não porque você mesmo o adicionou. No npm, as dependências transitivas são registradas no seu arquivo lockfile e, uma vez instaladas, aparecem em node_modules, mas elas não estão declaradas no arquivo do seu projeto package.json.
Qual é a diferença entre dependências diretas e transitivas?
As dependências diretas são os pacotes que seu projeto declara em seu próprio package.json. As dependências transitivas são todos os pacotes que esses pacotes dependem, em todos os níveis da árvore. Você controla os intervalos de versão das dependências diretas, enquanto os intervalos das dependências transitivas são definidos pelos pacotes pai.
Como faço para ver minhas dependências transitivas no npm?
Correr npm ls --all para imprimir sua árvore completa de dependências. Para descobrir por que um pacote específico está instalado, execute npm explain <package>, que mostra todos os caminhos de dependência que levam a ele.
Como faço para corrigir uma vulnerabilidade em uma dependência transitiva?
Primeiro, verifique se a atualização da dependência direta acima dela traz a versão corrigida. Se o intervalo de versões do pacote pai bloquear a correção, você pode usar as substituições do npm, disponíveis desde a versão 8.3, para forçar uma versão corrigida, embora você esteja executando uma combinação que o mantenedor do pacote pai provavelmente não tenha testado. Se não houver uma versão corrigida porque o pacote não é mais mantido, suas opções são uma correção retroportada, seu próprio fork ou patch, ou substituir o pacote que depende dele.
As dependências transitivas representam um risco à segurança?
Sim. Qualquer código deles que seja executado obtém o mesmo acesso que sua compilação ou seu aplicativo; alguns deles executam scripts de instalação, e um único pacote de nível superior pode trazer centenas deles. Ataques à cadeia de suprimentos, incluindo o “event-stream” em 2018 e o comprometimento do Mastra em 2026, entregaram sua carga maliciosa por meio de uma dependência transitiva recém-adicionada.
O que é risco hereditário?
O risco herdado é a exposição à insegurança que um projeto assume devido aos pacotes que acompanham as dependências que ele escolheu. Ele abrange vulnerabilidades, código malicioso e pacotes abandonados em qualquer ponto da árvore de dependências transitivas.
Fontes
- SecureChain para npm, TuxCare
- SecureChain para software de código aberto, TuxCare
- Suporte contínuo ao longo do ciclo de vida, TuxCare
- Relatório sobre o Gerenciamento de Dependências 2022, Endor Labs
- 145 pacotes npm do Mastra foram comprometidos por meio de uma conta de colaborador invadida, The Hacker News
- Pacotes npm da Mastra foram comprometidos, Socket
- Vulnerabilidade no módulo event-stream do Node.js, The Hacker News
- Como substituir as dependências da sua dependência, Stefan Judis
- npm ls, Documentação do npm
- npm explain, Documentação do npm
- npm dedupe, Documentação do npm
- npm ci, Documentação do npm
- npm audit, Documentação do npm
- Substituições no `package.json`, Documentação do npm
- package-lock.json, Documentação do npm
- Intervalos de Caret, node-semver
