Por que os gerenciadores de pacotes ignoram versões corrigidas de pacotes

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 compilações com correções aparecem acima do código original no Maven e no Python, mas abaixo dele em npm e o Gradle.
  • Em oito verificações de restrições realizadas em quatro gerenciadores de pacotes, uma compilação corrigida foi aprovada apenas três vezes.
  • A visibilidade do resolver — ou seja, se ele seleciona uma compilação corrigida por conta própria — determina se uma correção chega a ser lançada.
  • A abordagem que funciona em todos os casos consiste em fixar explicitamente a versão corrigida e recompilar o que será lançado.

Eis um cenário que já vimos se repetir mais de uma vez. Uma equipe faz o backport de uma correção de segurança para uma biblioteca que não pode ser atualizada, publica a versão corrigida em seu registro interno e segue em frente. A integração contínua (CI) está com o status “verde”. Uma semana depois, o scanner ainda sinaliza o CVE e, quando alguém finalmente abre o arquivo de bloqueio, a versão corrigida não está lá. Nada falhou. O gerenciador de pacotes simplesmente analisou a versão corrigida, decidiu que ela não se qualificava e instalou a versão vulnerável em seu lugar.

Isso não é um bug no npm, ou o Gradle, ou qualquer outra coisa. Cada ecossistema está fazendo exatamente o que as regras de versão determinam. O problema é que essas regras foram escritas para mantenedores de código-fonte que lançam versões, não para quem lança uma correção sobre a versão de outra pessoa. Distribuir pacotes de código aberto corrigidos entre ecossistemas é uma parte importante do que fazemos no SecureChain, e a lição mais valiosa que aprendemos é que o número de versão que você atribui a uma correção é tão importante quanto a própria correção.

O que significa “visibilidade do resolvedor”

A visibilidade do resolvedor refere-se à capacidade de um gerenciador de pacotes selecionar, por conta própria, uma compilação corrigida, considerando as restrições de versão já declaradas por um projeto. Uma compilação com alta visibilidade do resolvedor é selecionada pelos intervalos e pins existentes sem que seja necessário editar o manifesto. Uma compilação com baixa visibilidade do resolvedor permanece no registro, totalmente disponível e totalmente ignorada, até que alguém a force manualmente.

A visibilidade depende de dois fatores. O primeiro é a ordenação, ou seja, se o ecossistema considera que sua versão corrigida é mais recente ou mais antiga do que a versão original na qual se baseia. O segundo é a correspondência, ou seja, se restrições comuns, como um intervalo de caret ou um pin exato, consideram a versão corrigida aceitável. Essas duas questões recebem respostas diferentes em cada ecossistema, e é por isso que uma estratégia que funciona perfeitamente em um lugar acaba falhando discretamente no seguinte.

Uma versão, quatro veredictos diferentes

Para ilustrar melhor, pegamos um único pacote hipotético na versão upstream 4.7.3, além de uma versão corrigida do mesmo lançamento, e executei ambas por meio da lógica de versão real que cada ecossistema utiliza. Isso significava que npmpróprio semver biblioteca (7.6.0), o comparador de versões do Maven 3.9.11, o Gradle 8.14 fazendo a resolução em um repositório local e o Python’s packaging biblioteca, que é o que pip usa para avaliar versões. A compilação corrigida foi escrita como 4.7.3-patched.2 em todos os lugares, exceto no Python, onde era 4.7.3+patched.2, já que essa é a forma prevista pela especificação do Python para essa tarefa. Cada execução representava uma nova resolução, sem nenhum arquivo de bloqueio atrapalhando.

Em seguida, fizemos duas perguntas a cada um. Com um intervalo normal de compatibilidade definido (^4.7.0 para npm, [4.7,4.8) para o Maven e o Gradle, ~=4.7.0 (para Python), qual versão é a melhor? E com 4.7.3 Se ambas forem declaradas como versões exatas, qual delas prevalece?

Ecossistema Classificação das versões corrigidas Seleções de produtos compatíveis Seleções do Exact 4.7.3
npm Abaixo de 4.7.3 Upstream 4.7.3 Upstream 4.7.3
Maven Acima de 4.7.3 Versão corrigida Upstream 4.7.3
Gradle Abaixo de 4.7.3 Upstream 4.7.3 Upstream 4.7.3
Python (pip) Acima de 4.7.3 Versão corrigida Versão corrigida

Três vitórias em oito, e duas delas são do Python. O Maven e o Gradle leem os mesmos arquivos POM dos mesmos repositórios e, mesmo assim, chegaram a conclusões opostas sobre qual versão é mais recente. Se isso te surpreende, você não está sozinho. Isso também surpreendeu algumas pessoas da nossa equipe.

Por que o npm trata sua correção de segurança como uma versão beta

npm segue o Sistema de Versões Semânticas (SemVer), e o SemVer estabelece que um hífen após o número do patch indica o início de uma pré-versão. As pré-versões são classificadas abaixo da versão oficial que elas antecedem, portanto 4.7.3-patched.2 é considerado mais velho do que 4.7.3. No que diz respeito a npm Nesse sentido, sua compilação reforçada é uma versão candidata à lançamento da versão vulnerável.

A correspondência só piora a situação. Por padrão, npm A correspondência de intervalos ignora completamente as versões pré-lançamento, portanto ^4.7.0 e ~4.7.0 nunca selecionará a compilação corrigida. Mesmo um intervalo escrito como ^4.7.3-patched.0, que opta explicitamente por receber pré-lançamentos, ainda prefere o formato simples 4.7.3 quando ambos estiverem disponíveis, porque 4.7.3 é “mais recente”.

O problema que realmente afeta as equipes é mais sutil do que isso. Digamos que seu registro interno contenha o arquivo corrigido 4.7.3 construção e uma antiga 4.7.2, mas não a montante 4.7.3. Seria de se esperar que ^4.7.0 para pousar na área. Em nosso teste, o resultado foi 4.7.2, a versão mais antiga e ainda vulnerável, porque 4.7.2 era a versão mais recente disponível que não fosse de pré-lançamento. A correção estava bem ali e npm passou por cima dele.

Então, em npm, a versão corrigida precisa ser nomeada explicitamente. Se foo é uma dependência direta; isso significa apenas indicar a versão exata em dependencies. Para as cópias que se encontram mais abaixo na árvore, npm as substituições são a ferramenta, configuradas na raiz package.json do aplicativo:

{
  "overrides": {
    "foo": "4.7.3-patched.2"
  }
}

Isso faz com que todas as cópias de foo em qualquer ponto da árvore de código da versão corrigida, seja transitivo ou não, e npm ls foo vai indicar se ficou preso. Vale a pena conhecer alguns pontos importantes. npm O `only honors` só considera substituições no projeto raiz; portanto, uma biblioteca não pode defini-las para seus usuários. Se foo também é uma dependência direta; sua entrada em dependencies deve corresponder exatamente à substituição (o "$foo" o formulário de referência mantém os dois sincronizados), ou npm não consegue ser instalado com um EOVERRIDE erro. E a nave de substituição permanece estacionada na antiga base. Assim que as naves a montante 4.7.4, qualquer projeto que não contenha a substituição irá, sem problemas, saltar para ela por meio de um intervalo de seleção, e 4.7.4 não teremos seu backport se a correção nunca tiver sido aceita no upstream. É por isso que tratamos cada lançamento de patch do upstream como um gatilho para recompilar a correção com base na nova versão, de modo que 4.7.4-patched.1 existe antes mesmo de alguém precisar dele. O Yarn’s resolutions e pnpm‘s overrides seguem a mesma ideia, mas com sua própria sintaxe.

O Maven e o Gradle leem o mesmo POM e apresentam divergências

O comparador de versões do Maven reconhece uma pequena lista de qualificadores, como alpha, beta, rc, e snapshot, e tudo o que ele não reconhecer é classificado após a versão padrão. Portanto, no Maven, 4.7.3-patched.2 é mais recente que 4.7.3, e um intervalo como [4.7,4.8) escolhe isso. Boas notícias, na maioria das vezes.

O problema é que quase ninguém usa intervalos de versão no Maven. Uma versão simples em um POM é uma exigência não obrigatória, e o Maven resolve conflitos com base na distância da raiz da árvore, e não em qual versão é a mais recente. Portanto, se a declaração mais próxima indicar 4.7.3, você obtém 4.7.3, ponto final. A ordem do Maven é compatível com compilações com patches, mas sua resolução raramente leva em conta a questão da ordem. Para incluir a compilação com patches, você a declara em dependencyManagement no nível superior, o que se sobrepõe a qualquer solicitação da árvore transitiva.

O Gradle segue a lógica inversa na ordenação. Sua regra é que uma parte adicional não numérica torna a versão menor, e patched não é uma das poucas palavras especiais (rc, snapshot, release, sp, e alguns outros) que têm seu próprio lugar na fila. Então 4.7.3-patched.2 classificações a seguir 4.7.3. O Gradle também resolve conflitos escolhendo a versão mais alta no gráfico; assim, quando uma dependência solicita a compilação corrigida e outra solicita 4.7.3, nossa compilação de teste foi resolvida para o upstream 4.7.3. A versão vulnerável foi escolhida porque o Gradle a considerou mais recente. A correção é uma versão específica, declarada com strictly, seja na própria dependência ou dentro de uma restrição de dependência, como em version { strictly '4.7.3-patched.2' }. Uma restrição simples não funciona neste caso, porque o Gradle a trata apenas como um valor mínimo e 4.7.3 já está priorizada em relação à versão corrigida, na visão do Gradle.

O Python funciona bem, desde que você escolha o separador correto

A especificação de versão do Python, a PEP 440, possui algo que as outras não têm: um identificador de versão local destinado exatamente a essa situação. Qualquer coisa após o sinal de mais é um identificador local, e 4.7.3+patched.2 tipos acima 4.7.3. Melhor ainda, um pin público como ==4.7.3 ignora o rótulo local durante a correspondência; portanto, um projeto já fixado em 4.7.3 aceitará 4.7.3+patched.2 e prefiro isso. Não são necessárias alterações no manifesto. É a única linha da tabela em que ambas as respostas indicam a compilação corrigida. No entanto, os arquivos de bloqueio ainda têm peso. Um arquivo de bloqueio ou um arquivo de requisitos com hash fixado mantém a compilação antiga até que você a gere novamente, e um ambiente que já tenha 4.7.3 O que está instalado permanece no lugar até que alguém o desligue pip install --upgrade.

O problema é que, em vez disso, está sendo usado um post-release. 4.7.3.post1 é ainda mais alto, mas ==4.7.3 não corresponde a ela, portanto, todos os projetos vinculados à versão exata do upstream a ignoram silenciosamente. Se você estiver publicando compilações do Python com patches, o rótulo local é aquele que chega aos projetos vinculados. Usar uma versão pós-lançamento é a maneira de garantir que uma correção chegue apenas às pessoas que já estavam atualizando.

Há um detalhe prático a ser considerado. O PyPI não aceita uploads com rótulos de versão locais; portanto, as compilações com patches precisam ficar hospedadas em outro lugar, geralmente em um índice privado. A forma como esse índice é configurado também é importante do ponto de vista da segurança. pip não prioriza um índice em detrimento de outro. Ele simplesmente seleciona a melhor correspondência entre todas as fontes que lhe são indicadas. Portanto, indicar pip Utilizar um único índice que atenda tanto às suas compilações quanto aos pacotes do upstream é mais seguro do que adicionar um índice extra, o que abre espaço para confusão nas dependências.

O que isso significa se você enviar ou utilizar pacotes atualizados

A lição que tiramos de tudo isso é que não se pode contar com nenhum resolvedor para encontrar uma solução por conta própria. O Python é o que mais se aproxima disso, e mesmo assim depende da escolha da sintaxe da versão correta. Em todos os outros casos, a compilação corrigida precisa ser fixada explicitamente, seja por meio de uma substituição, um dependencyManagement entrada, ou uma versão estrita, e tudo o que for lançado precisa ser recompilado para que o pino esteja realmente presente. Se você é o responsável pelo aplicativo, trata-se de uma única alteração em um único local. Se você publica pacotes que outras equipes utilizam e a correção chega a três níveis abaixo na árvore, isso significa recompilar também os pacotes acima dela, em ordem, até que o artefato de nível superior contenha a versão corrigida, pois ninguém a jusante vai adicionar sua substituição por você. É mais trabalho do que publicar um único artefato, mas é a única abordagem que encontramos que se comporta da mesma maneira nos quatro ecossistemas.

A outra lição é parar de interpretar o status de segurança a partir da string de versão. Um sufixo indica ao resolvedor em que categoria uma compilação se enquadra. Ele não fornece nenhuma informação confiável sobre quais CVEs essa compilação realmente corrige, e muitos scanners irão detectar 4.7.3-patched.2, mapeá-lo de volta para 4.7.3, e sinalizar todos os CVEs conhecidos para essa versão. Um registro legível por máquina do que foi corrigido, como um documento VEX que acompanha a compilação, é o que permite que um scanner identifique a diferença, desde que o scanner realmente o leia.

Nada disso é incomum. É apenas a parte da segurança da cadeia de suprimentos que está nos mecanismos internos do gerenciador de pacotes, e é lá que muitas soluções eficazes acabam sendo ignoradas. Se você mantém versões que o desenvolvedor original deixou de oferecer suporte — e é exatamente aí que entra o Endless Lifecycle Support da TuxCare —, vale a pena dedicar uma tarde para verificar o que seus próprios resolvedores realmente instalam.

Perguntas frequentes

O que é visibilidade do resolvedor?

A visibilidade do resolvedor refere-se à capacidade de um gerenciador de pacotes selecionar, por conta própria, uma compilação corrigida, considerando as restrições de versão já declaradas por um projeto. Isso depende de como o ecossistema classifica a versão corrigida em relação à versão original e se restrições comuns, como intervalos e pins exatos, correspondem a ela.

Por que o npm não instala uma versão corrigida do pacote?

npm trata qualquer versão com um sufixo com hífen, como 4.7.3-patched.2, como pré-lançamento. Os pré-lançamentos são classificados abaixo do lançamento correspondente e são ignorados pelos intervalos normais definidos com os caracteres “caret” e “til”, portanto npm instala a versão do desenvolvedor original ou uma versão anterior.

Como forçar uma dependência transitiva corrigida no npm?

Adicione um overrides acesso à raiz package.json que indique a versão corrigida exata. npm em seguida, utiliza essa versão para todas as cópias do pacote na árvore de dependências, incluindo as transitivas. Se o pacote também for uma dependência direta, sua versão em dependencies deve corresponder à substituição, ou npm termina com um EOVERRIDE erro. Substituições em uma biblioteca package.json são ignorados quando essa biblioteca é instalada como dependência.

O Maven considera um qualificador de versão personalizado como mais recente do que a versão lançada?

Sim. O Maven classifica os qualificadores não reconhecidos após a versão simples, portanto 4.7.3-patched.2 é mais recente que 4.7.3. No entanto, o Maven geralmente resolve conflitos com base na distância na árvore de dependências, e não pela versão; portanto, você ainda precisa dependencyManagement para garantir que a versão corrigida esteja selecionada.

Por que o Gradle prefere a versão original em vez de uma compilação corrigida?

O Gradle trata uma parte adicional não numérica como -patched.2 como inferior à versão padrão, e seu mecanismo de resolução de conflitos seleciona a versão mais alta. Declare a compilação corrigida com strictly, seja na dependência ou dentro de uma restrição de dependência, para forçá-la.

O `pip` instalará a versão 4.7.3+patched.2 com um pin definido como ==4.7.3?

Sim, desde que a versão corrigida esteja em um índice pip como se pode ver. De acordo com a PEP 440, um pin público ignora os rótulos de versão locais ao fazer a correspondência, e uma versão local é classificada acima da pública; portanto, uma nova instalação seleciona a compilação corrigida. Um ambiente que já possui 4.7.3 necessidades pip install --upgrade, e é preciso regenerar um arquivo de bloqueio primeiro.

Resumo
Por que os gerenciadores de pacotes ignoram versões corrigidas de pacotes
Nome do artigo
Por que os gerenciadores de pacotes ignoram versões corrigidas de pacotes
Descrição
Uma versão corrigida, quatro gerenciadores de pacotes e apenas 3 das 8 verificações de restrições detectaram o problema. Veja a seguir o motivo e como garantir que a correção seja aplicada.
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.