Seu registro privado do npm tem um ponto cego | TuxCare

Os patches do php2shell (CVE-2026-109618 e CVE-2026-109619) para o PHP em fim de vida (EOL) já estão disponíveis. Como as respostas do atendimento de vendas estão mais demoradas do que o normal devido à alta demanda, a maneira mais rápida de obter os pacotes corrigidos é adquirir o ELS para Runtimes on-line (abre em uma nova aba). Leia as perguntas frequentes sobre os patches

Vá direto para um resumo gerado por IA desta postagem do blog

  • Resuma esta postagem com o Perplexity
  • Resuma esta postagem com o Gemini

Principais conclusões

  • As dependências de URL do Git e de arquivos tar remotos são obtidas diretamente de seus respectivos locais, nunca por meio do seu repositório.
  • Das 53.707 versões de pacotes que acompanhamos, 811 especificações de dependência são resolvidas fora do registro.
  • Referências de branch, tag e não fixadas representam o verdadeiro risco, já que seu código pode sofrer alterações sem que haja um lançamento.
  • Uma reconstrução byte a byte comprova a fidelidade, não a contenção; portanto, um escape reproduzido fielmente continua sendo um escape.

A maioria das equipes que opera um servidor privado npm O registro descreve isso da mesma forma. Tudo passa pelo Nexus ou pelo Artifactory; o registro público fica atrás de um proxy, e nada chega ao laptop de um desenvolvedor ou a um executador de CI sem passar pela única porta que eles controlam.

Isso é verdade, em grande parte. É justamente esse “em grande parte” que vale a pena discutir.

Ao desenvolver o SecureChain, o serviço da TuxCare que recompila pacotes de código aberto a partir do código-fonte e os distribui com informações de proveniência, procuramos os casos em que isso não se aplica. Analisamos 53.707 versões do npm pacotes no gráfico de dependências, rastreamos e marcamos todas as dependências que indicam npm para evitar completamente o uso do registro. Esta postagem aborda o que descobrimos, por que a configuração do seu registro não consegue redirecionar essas dependências e o que você deve verificar em seus próprios projetos nesta semana.

O que entendemos por “dependência de escape”

Uma dependência que não está no espelho é aquela declarada com uma URL do Git, uma URL de um arquivo tar ou algum outro local remoto, em vez de um nome de pacote e um intervalo de versões, portanto npm baixa o arquivo diretamente desse local, em vez de resolvê-lo por meio do seu registro.

Aqui está um exemplo. Os nomes foram alterados, mas a estrutura é exatamente a mesma que encontramos nas dependências de tempo de execução de um pacote publicado:

"dependencies": {
  "lodash": "^4.15.0",
  "some-markdown-lib": "git://github.com/some-user/some-markdown-lib.git"
}

A primeira linha representa o caso normal. npm consulta o registro que você tiver configurado para um lodash versão que satisfaça ^4.15.0. A segunda linha não possui versão, nem commit, nem registro. Ela indica um repositório em uma conta pessoal do GitHub, e npm baixa o que quer que esteja no ramo padrão no momento da instalação.

Se isso git:// Se o prefixo parece inativo para você, bom olho. O GitHub desativou o protocolo Git sem autenticação já em 2022. Isso não significa que a dependência esteja inativa. npm normaliza o GitHub git:// URLs em HTTPS, de modo que, em qualquer configuração que permita dependências do Git, a resolução e a instalação continuem funcionando. Uma especificação que parece estar com defeito na revisão de código funciona bem na prática, o que é uma ótima maneira de algo passar despercebido por anos.

Por que as configurações do Registro não conseguem alterá-lo

O registry começando a se estabelecer .npmrc responde a uma pergunta. Quando uma dependência consiste em um nome e um intervalo, onde deve npm procurar? Um URL do Git ou um link para um arquivo tarball em HTTPS não é esse tipo de dependência. Ele contém sua própria localização, e npm vai diretamente para lá. Substituições de registro com escopo, tokens de autenticação, um repositório proxy na frente de npmjs.org: nada disso entra em cena, porque npm nem sequer faz uma solicitação de registro para essa dependência.

A profundidade também não ajuda. Se um pacote três níveis abaixo na sua árvore declarar um, sua instalação tenta acessar o GitHub no meio do processo, a partir de um laptop ou de um runner que você supunha que se comunicasse apenas com o seu registro. Quando essas máquinas não conseguem acessar o GitHub, a instalação falha, e é assim que muitas equipes descobrem que eles existem. Quando conseguem, nada falha e ninguém percebe.

Os arquivos de bloqueio ajudam menos do que as pessoas esperam. npm Ele registra o commit resolvido para uma dependência do Git; assim, instalações repetidas a partir do mesmo arquivo de bloqueio obtêm o mesmo código. Mas a primeira resolução ainda utiliza o que quer que o branch contivesse naquele dia, e cada comando `fetch` ainda acessa o GitHub, em vez do seu registro. Um arquivo de bloqueio fixa o conteúdo. Ele não altera a origem do conteúdo.

O que 53.707 versões de pacotes nos revelaram

Entre essas 53.707 versões de pacotes, encontramos 811 especificações de dependências que são resolvidas fora do registro, distribuídas por 682 versões de 303 pacotes distintos. O GitHub é responsável por 730 delas. O restante é uma mistura heterogênea de links de CDN, servidores de download de fornecedores, hospedagem pessoal e um gist do GitHub utilizado como dependência.

Das 811 especificações, 636 são dependências de desenvolvimento. Os usuários do pacote publicado normalmente não as instalam, embora elas sejam baixadas sempre que o próprio pacote é compilado, o que é importante se você se preocupa com compilações reproduzíveis.

Os outros 175 estão nas dependências de tempo de execução ou opcionais, os escopos voltados para o consumidor. Esse número precisa de um asterisco, e é melhor esclarecer isso agora do que ter alguém apontando isso mais tarde. Desses 175, 155 vêm de uma única ferramenta CLI abandonada há muito tempo, com dezenas de versões publicadas e nada no gráfico de dependências que acompanhamos que dependa dela. Retire essa ferramenta e você ficará com 20 especificações de escape voltadas para o consumidor, distribuídas por 15 pacotes.

Vinte parece pouco, e de fato é. Ninguém deve interpretar isso como uma epidemia. Mas 13 desses 20 apontam para um branch ou um repositório vazio, o que significa que o código por trás deles pode mudar a qualquer momento. Números pequenos são exatamente o que permite que esse tipo de coisa passe despercebido. Ninguém audita os 15 pacotes que não sabia que eram especiais.

Nem toda fuga é igualmente perigosa

A forma do objeto é muito mais importante do que a direção para a qual ele aponta.

Uma dependência fixada a um SHA de commit completo é endereçada por conteúdo. O proprietário de um repositório não pode mover essa referência para um commit diferente, da mesma forma que pode fazer com um branch ou uma tag; portanto, mesmo que o repositório mude de mãos amanhã, você receberá o que estava lá no momento em que a fixação foi criada. O que resta é a questão da disponibilidade, além do fato de que isso ainda contorna o seu registro. Trata-se mais de um problema de contenção do que de injeção de código.

URLs de repositórios sem especificações adicionais, ramos, tags e referências Git com resolução semver devem ser tratados como mutáveis. A ausência total de referência indica o ramo padrão. Um #branch A referência segue o ramo. A #semver: O `range` é resolvido em relação a tags ou referências correspondentes no momento da instalação. E as tags merecem uma menção à parte, porque as pessoas as tratam como se fossem versões. Mas não são. Uma tag é um ponteiro, e o proprietário do repositório pode movê-la para um commit diferente sempre que quiser. Se não for um SHA, considere que ela pode mudar.

Os URLs de arquivos tar remotos também devem, em geral, ser colocados no bucket mutável, a menos que haja algo que garanta sua integridade de forma independente. Os arquivos-fonte gerados pelo GitHub já tiveram suas somas de verificação alteradas antes, sem que ninguém tivesse mexido no repositório; portanto, mesmo um link que pareça estável não garante que você receberá os mesmos bytes.

Em nosso censo, a divisão foi desigual. De acordo com essa classificação, 687 das 811 especificações eram mutáveis e apenas 124 estavam vinculadas a um SHA. Não contabilizamos file:, link:, workspace:, nem mesmo caminhos relativos. Havia 1.061 deles, e nós os excluímos porque nunca acessam a rede; portanto, não podem escapar de nada.

Agora veja o que o caso mutável não exige. Nada de invasão de conta, nada de typosquatting, nada de token de publicação roubado. O dono desse fork pode fazer um push para seu próprio branch padrão numa terça-feira à tarde, e a próxima instalação nova em qualquer lugar vai pegá-lo. Não há aumento de versão, nem evento de publicação, e nada de novo no registro para um scanner analisar. Se a URL apontar para um domínio que alguém se esqueceu de renovar, você nem precisa do proprietário. Quem quer que registre o domínio em seguida pode obter um certificado válido para ele e decidir o que sua instalação vai baixar.

Uma construção sustentável não resolve a questão

Essa foi a parte que mais nos incomodou.

Um pipeline de recompilação verifica a fidelidade. Compilamos a partir do código-fonte, compactamos o arquivo tar e confirmamos se ele corresponde, byte a byte, ao arquivo tar do upstream. Se houver correspondência, sabemos que nossa recompilação não alterou nem um único byte do que foi lançado pelo upstream. Essa é uma garantia valiosa, e vamos defendê-la perante qualquer pessoa.

Também não é a garantia que as pessoas costumam supor. Fidelidade significa que reproduzimos fielmente a versão original. Se o manifesto da versão original indicar para buscar algo de um fork pessoal, uma reconstrução fiel dirá exatamente a mesma coisa. Uma falha reproduzida fielmente continua sendo uma falha.

A proveniência tem o mesmo limite. Um atestado SLSA informa como um artefato foi construído, onde e a partir de qual fonte. Ele não informa o que esse artefato solicitará ao gerenciador de pacotes de um cliente para baixar posteriormente, em uma máquina diferente, semanas após a conclusão da compilação. Esse é o escopo que o SLSA define para a proveniência, e é um escopo sensato. A proveniência atesta como algo foi feito, nunca se deveria ter sido feito.

Em vez disso, você poderia monitorar a rede durante as compilações, e algumas iniciativas de recompilação fazem exatamente isso. Isso detecta coisas que uma verificação do manifesto nunca detectaria, como um postinstall script que baixa um arquivo binário de algum lugar. Mas ele descreve apenas a sua compilação. A instalação que realmente importa ocorre no computador de outra pessoa, mais tarde, e o seu proxy de compilação nunca tem acesso a ela. Cada abordagem subleva um ponto cego da outra.

A lição que tiramos de tudo isso é que fidelidade e contenção são questões diferentes e exigem verificações distintas. Se você incluir uma verificação de contenção na sua etapa de verificação, uma falha não indicará mais qual das questões falhou.

A correção que quase piorou a situação

A solução óbvia é espelhá-lo. Copie a dependência do Git para o registro como um pacote adequado, reescreva a especificação para um intervalo de versões normal e siga em frente.

Mas tem um porém. No momento em que você edita package.json, seu arquivo tarball não está mais em conformidade com o código original, e a verificação de fidelidade o rejeita, com razão. Você acaba tendo que escolher entre três coisas que deseja: fidelidade byte a byte, contenção real e pacotes que possam ser instalados como substitutos diretos, sem que ninguém precise alterar seu código. Você consegue duas.

Dar um nome à cópia acabou se revelando uma armadilha. Em um caso, a dependência do Git apontava para um fork pessoal criado há anos, cujo package.json declarou um nome de pacote em letras maiúsculas e uma versão que nunca havia sido publicada em lugar algum. npm não aceita novos nomes de pacotes com letras maiúsculas, então esse nome estava descartado. O nome em letras minúsculas já estava em uso no registro público, por um fork diferente, com um número de versão inferior. Se publicarmos nossa versão com esse nome e um número de versão superior, um resolvedor configurado para comparar candidatos entre fontes privadas e públicas poderia dar preferência ao nosso.

Isso se assemelha bastante à receita clássica para a confusão de dependências. Teríamos criado exatamente o tipo de problema que estávamos tentando resolver.

Uma solução clara em um caso como esse é criar um novo nome com escopo restrito que não entre em conflito com nada público, além de reescrever o manifesto do consumidor para que ele aponte para lá. Dá mais trabalho e significa que o pacote deixa de acompanhar automaticamente o branch de outra pessoa. Chegamos à conclusão de que essa última parte é, na verdade, um recurso. Acompanhar silenciosamente o branch padrão de um desconhecido era a vulnerabilidade, não um serviço.

O que verificar em seus próprios projetos

Comece por encontrá-los. npm pode fazer isso para você com o comando de consulta:

npm query ":type(git), :type(remote)"

Isso percorre sua árvore de dependências instalada e retorna tudo o que veio de um repositório Git ou de uma URL remota, incluindo dependências transitivas. Quando ele encontrar algo, você vai querer saber quais resultados são diretos e quais vieram de três níveis abaixo.

Se você já se mudou para npm 12, você provavelmente já se deparou com alguns desses problemas da maneira mais difícil. As dependências do Git e de URLs remotas agora estão bloqueadas por padrão, e uma instalação que encontre uma delas falhará até que você as habilite com allow-git ou allow-remote. Essa é uma boa configuração padrão, e que já deveria ter sido adotada há muito tempo. É tentador definir allow-git=all Então, o pipeline volta a ficar verde, mas isso faz com que você volte exatamente ao ponto de partida, só que agora há uma linha de configuração que dá a impressão de que foi uma decisão deliberada. Se as únicas dependências do Git de que você precisa estiverem no seu próprio package.json, o root O valor limita a exceção a esses casos e mantém bloqueadas as exceções transitivas. pnpm O 11 faz praticamente a mesma coisa logo de cara. Seu blockExoticSubdeps agora, a configuração padrão é true, que bloqueia especificações do Git e de arquivos tarball nas dependências transitivas, mas continua permitindo aquelas que você mesmo declara.

Mas observe onde esse controle está. É o cliente que se recusa a buscá-las, não o fato de seu registro conter alguma coisa. Seu registro privado continua sem ter acesso a essas dependências. O cliente simplesmente se recusa a buscá-las.

Seja qual for o resultado da consulta, observe o formato antes de mais nada. Um pin SHA em uma dependência de desenvolvimento geralmente é um problema de contenção de menor prioridade. Um branch, uma tag ou a URL de um repositório bare em uma dependência de tempo de execução é o que deve ser corrigido, seja migrando para uma versão publicada, se houver, seja incorporando uma cópia fixada sob um nome com escopo de sua propriedade. E se seus executores de CI puderem acessar github.com De qualquer forma, vale a pena questionar se isso é realmente necessário, já que o controle de saída detecta essas consultas independentemente do que o manifesto indique.

E o que isso significa para o espelho?

Nada disso torna os registros privados inúteis. Eles realizam um trabalho significativo. Apenas respondem a uma pergunta mais restrita do que aquela que as pessoas lhes fazem. “Este pacote veio de algum lugar que controlamos?” não é a mesma pergunta que “Tudo o que este pacote precisa vem de algum lugar que controlamos?”

Essa segunda pergunta surgia constantemente enquanto estávamos desenvolvendo o SecureChain, e mudou a forma como interpretamos um package.json. Agora tratamos o formato de cada especificação de dependência como um sinal de segurança, juntamente com o intervalo de versões.

Se a sua equipe tiver npm dependências que não podem ser facilmente substituídas — esse é o problema que a TuxCare se propõe a resolver. O SecureChain abrange pacotes mantidos ativamente, recompilados a partir do código-fonte com rastreabilidade, e o Endless Lifecycle Support abrange as estruturas em fim de vida útil para as quais o desenvolvedor original deixou de fornecer correções, mas das quais sua aplicação ainda depende.

Perguntas frequentes

O que significa quando uma dependência sai do registro?

Isso significa que um pacote declara uma de suas dependências por meio de uma URL do Git, uma URL de um arquivo tar ou outro local remoto, em vez de um nome e um intervalo de versões. npm resolve essa dependência diretamente a partir do local indicado na especificação, em vez de por meio do registro configurado em .npmrc, a menos que o cliente esteja configurado para bloquear esse tipo de fonte. De qualquer forma, um registro privado ou um proxy não tem controle sobre isso.

Um registro privado do npm oferece proteção contra dependências do Git?

Não. O roteamento do registro se aplica apenas a dependências declaradas por nome e intervalo de versões; portanto, uma dependência do Git ou de uma URL remota nunca passa pelo seu registro privado. O que pode impedir isso é o npm o próprio cliente. npm Por padrão, o 12 blocks bloqueia dependências do Git e de URLs remotas por meio de seu allow-git e allow-remote configurações, que são um recurso independente do Registro.

O npm 12 bloqueia dependências do Git?

Sim, por padrão. npm 12 conjuntos allow-git e allow-remote para none, portanto, as dependências do Git e das URLs remotas, incluindo as transitivas, não serão instaladas, a menos que você opte por fazê-lo. A configuração allow-git=root permite apenas as dependências do Git declaradas no seu próprio package.json, enquanto all permite que elas apareçam em qualquer lugar na árvore.

Uma dependência do Git vinculada ao SHA de um commit é mais segura?

Sim, é muito mais seguro do que uma referência a um branch ou tag, pois o proprietário não pode associar um hash de commit a um código diferente. No entanto, isso ainda contorna o seu registro e depende da disponibilidade contínua do repositório; portanto, trate isso como algo a ser monitorado, e não como algo a ser ignorado.

A proveniência SLSA significa que um pacote é seguro para instalação?

Não. A proveniência SLSA atesta como e onde um artefato foi criado e de qual fonte. Ela não garante a autenticidade do código-fonte em si, nem abrange as dependências que o pacote obtém de fora do registro quando alguém o instala posteriormente.

Resumo
Seu registro privado do npm tem um ponto cego | TuxCare
Nome do artigo
Seu registro privado do npm tem um ponto cego | TuxCare
Descrição
As dependências do Git e de URLs ignoram completamente o seu registro privado do npm. Analisamos 53.707 versões de pacotes para verificar com que frequência isso ocorre.
Autor
Nome da editora
TuxCare
Logotipo da editora

Junte-se a mais de 4.500 profissionais de Linux e código aberto!

2 vezes por mês. Sem spam.

Índice
Obtenha as respostas sobre segurança de código aberto de que você precisa

Junte-se a mais de 4.500 profissionais de Linux e código aberto!

2 vezes por mês. Sem spam.