Risco na cadeia de suprimentos do Composer decorrente das tags mutáveis do Packagist

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

  • O Packagist indexa tags do Git em vez de hospedar artefatos; portanto, até julho de 2026, uma versão publicada poderia ser discretamente redirecionada para um commit diferente.
  • Dois incidentes ocorridos em 2026 utilizaram esse mesmo truque: um registrou um plugin malicioso do Composer e o outro sequestrou o autoloader do PHP.
  • Um comprometido composer.lock fixa o commit que já foi resolvido, de modo que uma reescrita posterior da tag não possa alterar o que composer install puxões.
  • O Packagist agora rejeita reetiquetagens de versões estáveis já publicadas, mas os arquivos de bloqueio antigos e os ramos de desenvolvimento ainda precisam ser verificados.

Todos os outros principais repositórios adotaram essa mesma política desde o início. Assim que uma versão é lançada, seu conteúdo fica congelado. Os arquivos tar do npm são imutáveis. O PyPI não permite que você reenvie um arquivo com o mesmo nome. O crates.io permite retirar, mas não excluir. O Maven Central não exclui de forma alguma um artefato publicado.

O Packagist tomou uma decisão diferente. Ele não armazena pacotes. Ele lê um repositório Git, indexa todas as tags que encontra e informa ao Composer de onde baixá-los. Uma versão como a 2.4.1 é uma tag do Git somada à referência de commit que o Packagist registrou para ela; e, até julho de 2026, essa tag poderia ser excluída e enviada novamente para um commit diferente, com o Packagist acompanhando tudo discretamente.

Essa vulnerabilidade foi a que os ataques à cadeia de suprimentos do Composer de 2026 exploraram, e o PHP sofreu dois exemplos concretos com três semanas de intervalo.

Por que o Packagist é um índice e não um registro

A diferença arquitetônica gera a diferença de segurança. Quando o Composer resolve uma dependência, o Packagist fornece metadados: o nome do pacote, suas versões e onde cada versão está armazenada, incluindo referências de código-fonte e de distribuição, que normalmente são um hash de commit. O código propriamente dito vem do repositório de código-fonte ou de um arquivo de distribuição que o host do código gera sob demanda a partir de uma referência específica. Nada nessa cadeia é um artefato imutável armazenado pelo próprio Packagist.

Registro Identidade da versão Uma versão lançada pode sofrer alterações?
npm Um arquivo tar publicado Não, as versões são imutáveis
PyPI Um arquivo enviado Não, os arquivos enviados não podem ser substituídos
crates.io Um arquivo publicado Não, só para americanos
Maven Central Um artefato publicado Não, a exclusão não é permitida
Packagist Uma tag do Git mais uma referência a um commit indexado Desde julho de 2026, as reclassificações não eram mais realizadas; antes disso, elas eram feitas

Essa última linha resume toda a questão do risco na cadeia de suprimentos do PHP, e não se trata de um bug que o Packagist introduziu acidentalmente. A indexação do Git mantém o atrito na publicação excepcionalmente baixo, e isso tem sido, há muito tempo, uma das vantagens práticas do Packagist.

Análise da realidade: Chamar isso de vulnerabilidade do Packagist não leva em conta onde realmente reside a mutabilidade. As tags do Git sempre foram movíveis por padrão. O Packagist herdou essa propriedade ao optar por indexar repositórios em vez de hospedar artefatos, e é justamente essa escolha que torna a publicação de um pacote do Composer relativamente leve.

Como é, na verdade, um ataque à cadeia de suprimentos de um compositor

A mesma tag de versão v5.0.2 foi redirecionada de um código legítimo para um código malicioso, sem registro de alterações e sem um novo lançamento

Em 30 de abril de 2026, alguém com acesso a uma conta de serviço comprometida enviou um commit malicioso para o repositório intercom/intercom-php e atribuiu a ele a tag 5.0.2 já existente, de acordo com o Aviso do GitHub. A versão reescrita composer.json declarou o pacote como um plug-in do Composer e, uma vez aprovado — seja por meio de um prompt interativo ou de um processo permissivo allow-plugins cenário – aconteceu durante composer install como um dropper que baixava o Bun runtime e executava um programa ofuscado para roubo de credenciais. O código malicioso ficou ativo por menos de duas horas antes que os mantenedores o corrigissem, restaurando o código limpo. Os pesquisadores o batizaram de Mini Shai-Hulud e o associaram à campanha que atingiu O cliente npm do Intercom na mesma janela.

Três semanas depois, a mesma vulnerabilidade foi explorada em uma escala muito maior. A partir das 22h32 UTC do dia 22 de maio, estendendo-se até o dia 23, um invasor com acesso de push em toda a organização reescreveu todas as tags existentes em quatro pacotes de localização do Laravel-Lang, direcionando-as para commits em um fork que ele controlava, de modo que nada malicioso chegou aos repositórios oficiais. O Socket contabilizou mais de 700 versões históricas remapeadas, o Aikido confirmou 233, e a principal onda de reescrita levou menos de 90 minutos. Desta vez, a carga maliciosa sequestrou o autoloader do PHP em vez de registrar um plugin.

Nenhum dos dois incidentes exigiu o lançamento de uma nova versão. Nenhuma entrada no changelog, nenhum novo número de versão, nada que um mantenedor ou um bot de dependências que monitora números de versão pudesse identificar como diferente.

Compare isso com a forma como um invasor precisa agir no npm. Substituir uma versão legítima lá significa lançar um número de versão diferente, pois os bytes por trás de um pacote e de uma versão existentes não podem ser trocados sem que você perceba. Uma nova versão aparece nos feeds de lançamento e aciona os bots de dependências. A alteração de uma tag não causou nada disso.

Etapa de ataque npm Packagist antes de julho de 2026
Publicar código malicioso É necessário um novo número de versão A versão existente poderia ser redirecionada
Evento de lançamento Sim Não
Detectado pelas ferramentas de comparação de versões Sim Muitas vezes ignorado, o número da versão nunca mudou
Revertido pelo desenvolvedor original A janela “Retirar da publicação” é aplicada A tag poderia simplesmente ser movida de volta

A reversão teve dois lados. A recuperação foi rápida — um mantenedor poderia simplesmente mover a tag de volta, o que foi exatamente o que aconteceu com o intercom-php naquela mesma noite —, mas a análise forense ficou mais difícil pelo mesmo motivo, pois um número de versão poderia apontar para um commit diferente do que costumava apontar. O registro público de transparência do Packagist registrou essas alterações de referência, o que, pelo menos, deixa um registro duradouro de quando uma referência publicada foi movida.

Duas cargas úteis diferentes, uma tag mutável

Uma tag do Git movida que chega à execução do código por meio de um plugin do Composer no momento da instalação ou do arquivo `autoload.files` a cada solicitação

Os dois incidentes de 2026 foram ataques à cadeia de suprimentos do Composer no sentido mais estrito, e ambos começaram com o mesmo artifício. O que aconteceu a seguir foi completamente diferente, e essa distinção é muito importante na hora de decidir o que realmente precisa ser corrigido.

A tag comprometida no intercom/intercom-php apontava para um commit que alterou composer.json para declarar o pacote como um plug-in do Composer. Essa é a versão desse ataque mais frequentemente abordada nos artigos, pois se encaixa perfeitamente no sistema de plug-ins do Composer. Um plug-in autorizado é executado durante composer install e composer update, antes que qualquer parte do código do seu aplicativo seja executada.

A vulnerabilidade Laravel-Lang nunca afetou o registro de plugins. Ela conectou um código malicioso src/helpers.php para o autoload.files entrada, de modo que a carga útil era executada em todas as solicitações PHP a partir do momento em que o pacote era instalado. A mesma tag “moved”, a mesma confiança que o Composer deposita no que quer que uma tag resolva, mas dois caminhos de execução completamente diferentes. Uma correção para um deles não resolve nada para o outro.

Você também pode conhecer as principais estratégias de proteção do framework em nosso guia detalhado sobre segurança no Laravel.

A segurança do plugin Composer apresenta uma falha real

Uma tag movida executa código. É preciso que algo a execute e, no caso do ataque do tipo “intercom”, esse algo é o sistema de plug-ins. Um pacote que declare "type": "composer-plugin" é carregado e pode executar código como parte dos próprios processos de instalação e atualização do Composer, assim que for autorizado. Esse é o comportamento esperado, já que os plug-ins existem para alterar a forma como o Composer instala os pacotes, o que significa que eles são executados antes que a instalação seja concluída.

A versão 2.2.0 do Composer adicionou o allow-plugins config, e a configuração padrão atual não permite absolutamente nada. Em composer.json fica assim.

{
  "config": {
    "allow-plugins": {
      "composer/package-versions-deprecated": true,
      "phpstan/extension-installer": true
    }
  }
}

Com allow-plugins Quando configurado dessa forma, o Composer se recusa a executar qualquer plugin que não esteja na lista. Um plugin que não conste na lista gera uma mensagem de aviso quando o Composer é executado de forma interativa; já em uma execução não interativa, o Composer apresenta uma falha em vez de continuar silenciosamente, a menos que o plugin se declare opcional. É melhor uma compilação com falha do que uma instalação silenciosa.

Cuidado: Verifique se alguém definiu "allow-plugins": true apenas para fazer com que uma mensagem de aviso desapareça. Essa única linha coloca todos os plug-ins, atuais e futuros, na lista de permissões e desativa completamente a proteção por lista de permissões. Executando composer config allow-plugins mostra o que realmente está configurado em um projeto e false é o valor que nega tudo.

Para uma compilação que não requer nenhum plug-in, os parâmetros são diretos e eficazes.

composer install --no-plugins --no-scripts --no-dev

Se sua versão de produção não precisar de plug-ins nem de scripts do Composer, desativar ambos nessa versão, mantendo-os disponíveis nos computadores dos desenvolvedores, é uma divisão razoável.

Os arquivos de carregamento automático são a opção mais discreta

allow-plugins não resolve o problema do ataque do tipo Laravel-Lang. autoload.files não é um recurso opcional que você possa restringir por meio de uma solicitação de permissão. É uma forma comum de os pacotes PHP fornecerem funções auxiliares, e o Composer precisa confiar na configuração de carregamento automático de cada pacote para que o ecossistema funcione.

Foi isso que fez com que essa versão do ataque tivesse um impacto tão grande. O Composer pergunta se você confia em um pacote para ser executado como um plug-in. Ele nunca pergunta se você confia em um pacote para definir arquivos carregados automaticamente. Antes da política contra malware do Composer 2.10 e da mudança de imutabilidade do Packagist, a principal proteção aqui era simplesmente nunca resolver a tag reescrita, e era por isso que o arquivo de bloqueio era tão importante.

A única coisa em que o Composer acerta e o npm não

Os scripts do Composer declarados por uma dependência não são executados. Apenas os scripts do pacote raiz são executados. Um script malicioso post-install-cmd Colocá-lo quatro níveis abaixo na sua árvore não faz absolutamente nada. Essa é uma configuração padrão significativamente melhor do que a do npm, onde qualquer pacote na árvore pode incluir um script pós-instalação que é executado durante a instalação.

Mecanismo Execuções para dependências
Scripts de ciclo de vida do npm Sim, qualquer pacote na árvore
Python .pth ganchos de inicialização Sim, na inicialização do interpretador
Ferrugem build.rs Sim, durante a compilação
Scripts do Composer Não, apenas o pacote raiz
Plug-ins do Composer Sim, se for permitido

Você sabia? Ignorar os scripts de dependência é uma decisão deliberada do projeto do Composer, e não uma medida de segurança adicionada à toa após um incidente. Esse é um dos detalhes mais negligenciados na comparação entre os dois ecossistemas.

O que o arquivo de bloqueio do Composer registra e o que ele deixa de registrar

composer.lock é mais protetor do que sua reputação sugere, e essa proteção tem um limite específico, que por acaso abrange tanto o ataque por plug-in quanto o ataque por carregamento automático, pois atua um nível abaixo de ambos.

Para cada pacote, o arquivo de bloqueio registra a versão resolvida juntamente com as referências de código-fonte e distribuição, normalmente o hash do commit do Git correspondente a essa versão. Execute composer install e o Composer solicita exatamente esse commit, em vez do que quer que a tag aponte atualmente; assim, uma reescrita posterior da tag nunca entra em cena. A garantia diz respeito a qual commit é solicitado, e não a uma soma de verificação dos bytes que são retornados, já que os hosts de código geram esses arquivos sob demanda e o arquivo de bloqueio não contém nenhum hash utilizável para eles. As operações que reavaliam dependências eram o ponto de vulnerabilidade, e isso inclui atualizações parciais como composer update foo -W. O artigo sobre segurança do Packagist aborda o mesmo ponto sobre os arquivos de bloqueio, o que vale a pena saber, pois boa parte da cobertura do Laravel-Lang afirmava que esses arquivos não ofereciam proteção alguma.

Operação Mantém a referência bloqueada Exposto a uma tag movida
composer install com o arquivo de bloqueio confirmado Sim Não
composer update Não, ele resolve novamente Sim
Atualização parcial de um pacote Apenas para os registros que ele não altera Sim, para qualquer coisa que seja resolvida novamente
composer require newpackage Em parte Sim, para o novo requisito e qualquer dependência que ele implique
Clone novo com o arquivo de bloqueio ignorado pelo Git Não Sim
composer install em uma compilação do Docker sem o arquivo lock copiado Não Sim

Um Dockerfile que copia apenas composer.json antes da execução, o `install` não tem um arquivo de bloqueio para trabalhar, e a proteção desaparece sem que ninguém tenha alterado uma única linha do código do aplicativo. Copiando composer.lock Além disso, isso restaura a proteção.

git ls-files composer.lock

Se isso não retornar nada, composer.lock não é rastreado neste repositório; portanto, a cada compilação limpa, as dependências são resolvidas novamente com base no que os metadados do registro indicarem naquele momento.

Triagem de malware e a mudança de julho relacionada à imutabilidade

Os incidentes ocorridos em maio não ficaram sem resposta. Nils Adermann e Igor Benko publicaram um roteiro de segurança para o Composer e o Packagist em 27 de maio, e o Composer 2.10 foi lançado no dia seguinte.

Um aspecto específico desse lançamento é diretamente relevante neste contexto. O Packagist já vinha importando os resultados da detecção de malware do Aikido desde março, e foi a versão 2.10 que tornou isso útil na prática. As versões sinalizadas são excluídas do conjunto de resoluções, portanto composer update, composer require e composer create-project não consigo entrar em contato com eles, e composer install gera um erro com aviso sonoro se uma versão marcada já estiver presente no seu arquivo de bloqueio, em vez de baixá-la silenciosamente. composer audit indica as mesmas versões e apresenta erro por padrão.

Em junho, as organizações do Private Packagist também passaram a contar com controles de restrição de plug-ins, permitindo que uma organização decida antecipadamente quais pacotes podem atuar como plug-ins do Composer. Com essa função ativada, uma versão corrompida que de repente se declare um plug-in é removida dos metadados de atualização e nunca chega ao resolvedor do Composer; no entanto, as instalações a partir de um arquivo de bloqueio existente também exigem a configuração separada de bloqueio de download do Private Packagist.

A grande mudança estrutural ocorreu em 7 de julho, quando o Packagist lançou as versões imutáveis. Assim que uma versão estável é publicada, a referência Git por trás dela não muda mais. Mesmo que a tag seja excluída, recriada ou movida no repositório original, o Packagist mantém a referência original, exibe um selo indicando que a reetiquetagem está bloqueada na página do pacote e envia um e-mail a todos os mantenedores.

As versões de desenvolvimento que acompanham ramificações continuam sendo atualizadas, já que esse é o objetivo de uma ramificação, e a regra se aplica ao mapeamento entre a versão e o commit, e não aos bytes do arquivo, pois a maioria dos arquivos ZIP ainda é gerada sob demanda pelo host do código. Alterações de referência bloqueadas, exclusões temporárias e recuperações são todas registradas no log público de transparência.

Nada disso limpa retroativamente um arquivo de bloqueio que tenha capturado um commit malicioso antes da implementação das proteções, e é exatamente por isso que composer.lock e allow-plugins Ainda é preciso verificar seus próprios projetos, independentemente do que o scanner indicar.

Verificação de pacotes antes que uma tag seja resolvida

Fluxo do SecureChain, desde o repositório de origem, passando pela recompilação, triagem de malware e geração de um pacote verificado, até sua aplicação

Em ambos os ataques, a referência perigosa entrou no gráfico de dependências no momento da resolução, muito antes de um plug-in ser carregado ou de um sequestro do autoloader ser acionado. O Composer 2.10 agora também inclui uma verificação de malware no momento da instalação, embora essa verificação ainda dependa de alguém ter sinalizado a versão primeiro.

O SecureChain para PHP oferece às equipes uma fonte de pacotes recompilada a partir do código-fonte e verificada contra malware, em vez de recorrer diretamente às referências do VCS upstream. Cada pacote é recompilado em um ambiente isolado e verificado antes de ficar disponível para instalação; assim, o que você baixa é exatamente o que foi verificado. As vulnerabilidades são corrigidas continuamente de acordo com o SLA, e o Suporte de Ciclo de Vida Ilimitado mantém essa cobertura mesmo depois que o mantenedor deixa de lançar atualizações. Isso é especialmente importante para pacotes abandonados, nos quais já não há mais a supervisão do mantenedor que detectaria uma alteração inesperada no upstream.

Nada disso substitui composer.lock, allow-plugins ou composer audit. Isso resolve a parte do problema que está a montante de tudo o que suas próprias ferramentas conseguem detectar. Veja como SecureChain para PHP funciona.

Perguntas frequentes

O que é um ataque à cadeia de suprimentos do Composer?

Um ataque que compromete um pacote PHP antes mesmo que ele chegue ao seu projeto, geralmente por meio de um repositório comprometido, uma tag do Git alterada ou um plugin malicioso do Composer, e não por meio de uma vulnerabilidade no código que você escreveu. Trata-se de uma versão específica do padrão mais amplo de ataques à cadeia de suprimentos que vem afetando registros de código aberto em todas as linguagens de programação.

A versão de um pacote do Packagist pode mudar após o lançamento?

Historicamente, isso era possível. O Packagist indexava tags do Git em vez de armazenar artefatos imutáveis; assim, uma tag podia ser excluída e enviada novamente para um commit diferente sem que o número da versão fosse alterado. Desde julho de 2026, o Packagist rejeita qualquer tentativa de alterar a referência de origem de uma versão estável já publicada — uma proteção do lado do servidor que se aplica independentemente da versão do Composer que o desenvolvedor utilize. As versões de desenvolvimento que acompanham ramos ainda podem ser alteradas, e essa mudança não garante a segurança das instalações históricas caso elas tenham resolvido um commit malicioso anteriormente.

Os scripts de dependências do Composer são executados durante a instalação?

Não. Apenas os scripts do pacote raiz são executados. Um script malicioso post-install-cmd uma dependência declarada é ignorada, o que representa um padrão mais restritivo do que o do npm.

Como os plug-ins do Composer executam código?

Um pacote com "type": "composer-plugin" pode executar código como parte dos próprios processos de instalação e atualização do Composer, uma vez que tenha sido instalado e autorizado. O Composer 2.2.0 e versões posteriores exigem que os plug-ins sejam explicitamente incluídos na lista de permissões por meio do allow-plugins config.

O arquivo composer.lock oferece proteção contra a movimentação de uma tag do Git?

Sim, se o arquivo de bloqueio já contiver o commit legítimo, porque composer install usa essa referência bloqueada em vez de resolver a tag novamente. Isso não protege composer update, novo composer require chamadas ou compilações em que o arquivo de bloqueio não está presente.

O que acontece quando a variável `allow-plugins` é definida como `true`?

Isso permite todos os plug-ins do Composer, atuais e futuros, o que elimina totalmente a proteção. Substitua-o por uma lista explícita dos plug-ins específicos de que seu projeto precisa.

Resumo
Risco na cadeia de suprimentos do Composer decorrente das tags mutáveis do Packagist
Nome do artigo
Risco na cadeia de suprimentos do Composer decorrente das tags mutáveis do Packagist
Descrição
Um ataque à cadeia de suprimentos do Composer não precisa de um typosquat. Veja como as tags Git mutáveis do Packagist possibilitaram dois incidentes ocorridos em 2026.
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.