A versão resumida
O que aconteceu: Em 20 de agosto de 2026, um invasor utilizou a conta de um mantenedor legítimo no crates.io para publicar versões maliciosas do arrayref 0.3.10, do append-only-vec 0.1.9 e do internment 0.8.7. O código das bibliotecas em todas as três estava intacto. Cada uma delas adicionou uma dependência do `proc-macro1`, um typosquat do `proc-macro2` de David Tolnay, cujo arquivo `build.rs` reconstruía endereços do invasor a partir de base64, baixava um binário de segundo estágio via TLS sem verificações de certificado e o executava durante a compilação.
Impacto: o arrayref tem 245 milhões de downloads acumulados no crates.io, dos quais cerca de 53,7 milhões ocorreram nos últimos 90 dias, e 403 crates o listam como dependência direta. A maioria das equipes nunca o escolheu, já que ele está presente nas bibliotecas de criptografia, gráficos e blockchain. A análise da StepSecurity estima que o período de exposição do arrayref tenha sido de 86 minutos, desde a publicação às 07h15 UTC até a remoção do índice às 08h41 UTC. O append-only-vec e o internment são menores, com cerca de 4,5 milhões e 14,4 milhões de downloads no total, respectivamente, e foram removidos na mesma onda.
Status: o crates.io excluiu seis crates criados pelo invasor (proc-macro1, proc-macro-en, aovine, arone, aronenao, tinymember), removeu as três versões maliciosas e bloqueou a conta do mantenedor. A Equipe de Resposta à Segurança do Rust afirma que “o computador ou as credenciais do autor provavelmente foram comprometidos”. A limpeza do registro não limpa suas máquinas. Caches do Cargo, árvores de dependências fornecidas por terceiros, camadas de contêineres, imagens de CI e espelhos ainda podem contê-las. Se um host resolveu um novo gráfico de dependências naquela manhã que pudesse ter acessado uma dessas crates, trate-o como suspeito até que você verifique.
A versão completa
Isso me afeta?
Há duas questões importantes aqui, e elas têm respostas diferentes. A janela para adquirir as versões maliciosas se fechou quando o crates.io as removeu naquela manhã. A janela para executá-las, porém, não. A remoção e a exclusão não eliminam nada que já esteja no disco; portanto, um cache do Cargo, uma árvore de dependências fornecida por terceiros, um espelho de registro ou uma camada de contêiner que tenha retido uma dessas versões ainda pode incorporá-la a uma compilação posterior. A aquisição foi limitada no tempo. A execução é uma característica das suas máquinas.
Na aquisição, o grupo afetado é menor do que os números de download sugerem. Um arquivo Cargo.lock definido fixa versões exatas, e o Cargo mantém as dependências bloqueadas nas versões do arquivo de bloqueio sempre que possível; assim, um projeto compilado a partir de um arquivo de bloqueio inalterado permaneceu na versão 0.3.9 do arrayref e nunca passou para a 0.3.10. Isso também se aplica a um contêiner limpo. Um sistema de arquivos novo não altera nada se a compilação copiar um arquivo de bloqueio válido, o que é o padrão normal.
O que realmente colocou as equipes em risco foi a re-resolução enquanto as versões maliciosas estavam ativas, em um gráfico de dependências que poderia atingir um dos crates afetados. Qualquer pessoa que tenha executado o `cargo update` nessa situação ficou exposta, assim como qualquer pessoa cujo bot de dependências tenha aberto e mesclado uma solicitação de pull de atualização naquela manhã. O mesmo vale para quem executou a compilação sem o arquivo de bloqueio presente, o que representa o verdadeiro risco para contêineres, já que um `Dockerfile` que copia apenas o manifesto, ou um `.dockerignore` que exclui o `Cargo.lock`, realiza a resolução do zero todas as vezes. O mesmo vale para qualquer pessoa cujo manifesto tenha mudado naquela manhã e introduzido um novo caminho para o `arrayref`, pois o Cargo resolve novamente a parte afetada do gráfico e grava um novo arquivo de bloqueio. E qualquer pessoa que tenha executado o `cargo install`, que ignora um arquivo de bloqueio publicado, a menos que você passe a opção `–locked`.
Os autores de bibliotecas estavam mais expostos do que os autores de aplicativos, embora nenhuma regra exigisse isso. A equipe do Cargo retirou a antiga orientação de que “binários enviam arquivos de bloqueio, mas bibliotecas não” em agosto de 2023, e o comando `cargo new` rastreia o `Cargo.lock` para bibliotecas desde então; no entanto, muitos repositórios de bibliotecas são anteriores a essa mudança e ainda são resolvidos dinamicamente na integração contínua (CI). Além disso, um arquivo de bloqueio não é transmitido para os usuários finais. Seu arquivo de bloqueio não afeta os usuários do seu crate; apenas o seu `Cargo.toml` o faz.
Comece pela cache local do Cargo. A Equipe de Resposta à Segurança do Rust publicou esta verificação:
find ~/.cargo/registry/cache -type f -name'append-only-vec-0.1.9.crate'-o-name'arrayref-0.3.10.crate'-o-name'internment-0.8.7.crate'-o-name'proc-macro1-*.crate'-o-name'proc-macro-en-*.crate'-o-name'aovine-*.crate'-o-name'arone-*.crate'-o-name'aronenao-*.crate'-o-name'tinymember-*.crate' -print
Em seguida, verifique se há as mesmas versões nos seus arquivos de bloqueio e se há alguma entrada de “proc-macro1”. Uma linha de “proc-macro1” no arquivo Cargo.lock já é, por si só, um indício, pois o “proc-macro1” foi criado pelo invasor para essa campanha e já foi excluído do Registro.
O que é o ataque “arrayref e proc-macro1”
A preparação começou horas antes da disseminação da carga maliciosa. De acordo com a linha do tempo da StepSecurity, uma conta no GitHub chamada dtolney foi criada às 01h17 UTC, fingindo ser David Tolnay, que mantém o verdadeiro proc-macro2. Em seguida, às 01h25 UTC, foi criada uma conta correspondente no crates.io. Às 01h55 UTC, essa conta publicou o proc-macro1 1.0.106, uma cópia limpa do proc-macro2 contendo metadados falsificados do autor. Essa versão não realizou nenhuma ação maliciosa. Ela permaneceu lá por mais de cinco horas, acumulando um pequeno histórico no registro e parecendo um crate comum.
Às 07h11 UTC, a mesma conta publicou o proc-macro1 1.0.107. Essa foi a versão utilizada como arma. Quatro minutos depois, às 07h15 UTC, o arrayref 0.3.10 apareceu na conta do proprietário legítimo, contendo uma dependência do typosquat. O internment 0.8.7 veio em seguida, às 07h34 UTC, e o append-only-vec 0.1.9, às 07h37 UTC.
O relatório da Equipe de Resposta à Segurança do Rust, publicado no mesmo dia, descreve o recebimento de uma notificação às 07h15 UTC informando que o proc-macro1 era malicioso e observa que o popular crate arrayref havia sido republicado recentemente e apontava para ele. O crédito pela descoberta inicial vai para a Equipe de Pesquisa da Nextron Systems GmbH. Separadamente, um repórter que usa o pseudônimo jhobern registrou o problema no banco de dados de alertas da RustSec e afirma ter enviado um e-mail também para a equipe de segurança do Rust. A reconstrução da StepSecurity situa esse relatório da RustSec às 07h54 UTC, 39 minutos após a publicação maliciosa, o que sugere que os dois relatórios estão descrevendo caminhos de notificação diferentes, e não o mesmo.
A limpeza começou às 08h03 UTC com a exclusão do proc-macro1. Desde então, a Equipe de Resposta à Segurança do Rust publicou os horários exatos de remoção das três crates legítimas. A arrayref 0.3.10 foi retirada às 08h41 UTC, após 86 minutos online; a internment 0.8.7, às 09h04 UTC, após 90 minutos; e a append-only-vec 0.1.9, às 09h25 UTC, após 107 minutos. O registro do crates.io mostra que as versões válidas que o invasor havia retirado foram restauradas entre 09h15 e 09h26 UTC para todas as três crates.
Como o “Yank” se tornou a isca
Incidentes anteriores envolvendo o npm não tiveram nada semelhante ao que aconteceu a seguir. No mesmo minuto em que a versão 0.3.10 foi publicada, o invasor usou a conta do proprietário para retirar todas as versões recentes do arrayref em uma sequência automatizada, com intervalos de cerca de quatro segundos entre cada ação. A retirada no Cargo não exclui uma versão. Os arquivos de bloqueio existentes que já fazem referência a uma versão retirada continuam sendo compilados, e o resolvedor ignora as versões retiradas apenas quando está selecionando versões novas. O histórico de versões do crates.io registra que as versões 0.3.5 a 0.3.9 foram removidas entre 07:15:24 e 07:15:40 UTC pela conta do proprietário e, em seguida, restauradas entre 09:15 e 09:17 UTC por um membro da equipe do crates.io. O mesmo aconteceu com as versões 0.1.7 e 0.1.8 do `append-only-vec` às 07:38 UTC e com as versões 0.8.3 a 0.8.6 do `internment` às 07:37 UTC.
Isso transforma uma operação rotineira de manutenção do registro em uma alavanca de engenharia social, e seu alcance era limitado de uma forma que fazia com que funcionasse. A remoção não notifica ninguém, e um projeto já vinculado a uma versão removida continua sendo compilado com ela. O aviso sobre a versão removida aparece assim que o Cargo se comunica com o índice do registro, em comandos como `cargo update`, `cargo add` ou `cargo install --locked`, de modo que chega primeiro às pessoas mais próximas de realizar uma nova resolução. Para elas, a versão maliciosa se tornou a versão mais recente que o Cargo aceitaria e, para requisitos comuns de compatibilidade, como ^0.3.5, era a única candidata restante que não havia sido removida. Um desenvolvedor que visse o aviso e executasse o `cargo update` para eliminá-lo poderia, com razão, pensar que estava agindo de forma responsável.
No interior do script de compilação
O código malicioso estava no arquivo `build.rs` do `proc-macro1`, e não no `arrayref`. Qualquer pessoa que tivesse acessado o código-fonte do `arrayref` 0.3.10 para verificar teria encontrado as mesmas macros de referência a arranjos que o crate sempre contou, além de uma linha de dependência no `Cargo.toml`.
O script de compilação montou seus detalhes de comando e controle a partir de fragmentos em base64, em vez de codificar diretamente uma URL legível, e, em seguida, baixou um binário de segundo estágio do endereço https://23.254.165.112:9089/ por meio de TLS, com a validação de certificado explicitamente desativada. O downloader utilizou o rustls com um verificador do tipo “aceitar tudo”, cujos métodos de verificação retornavam sucesso incondicionalmente; assim, a conexão era criptografada, mas não oferecia autenticação efetiva do servidor. Um segundo endereço no mesmo host, 23.254.165.112:443, foi passado à carga útil como seu ponto de extremidade de comando e controle. O nome de host associado relatado pela StepSecurity é hwsrv-798836.hostwindsdns.com.
A execução variava de acordo com a plataforma. Em sistemas Unix, a carga útil era gravada em /tmp/rust-setup, tornada executável e iniciada como um processo independente. No Windows, ela salvava o arquivo %TEMP%\rust-setup.ps1 e o executava por meio do arquivo %TEMP%\rust-setup-launch.vbs no wscript.exe com o sinalizador CREATE_NO_WINDOW, de modo que nada aparecia na tela.
Dois fatores mantiveram isso oculto. Primeiro, como observa a StepSecurity, o código da biblioteca em proc-macro1 é, na verdade, o proc-macro2; portanto, as compilações são bem-sucedidas e nada de anormal aparece na saída normal. Segundo, nada em seu programa precisava chamar uma função arrayref, e você não precisava executar especificamente o `cargo build`. O Cargo não possui um gancho de instalação, portanto nada era executado no momento do download, mas um script de compilação é executado imediatamente antes da compilação do pacote para qualquer comando que compile o gráfico. Os comandos `cargo check`, `cargo test`, `cargo clippy` e `cargo doc` eram suficientes, cada um no nível de privilégio de quem ou do que os iniciasse. Em um laptop de desenvolvedor, trata-se de uma sessão de usuário que pode ter acesso a chaves SSH, credenciais de nuvem e dados do navegador. Na CI, trata-se de um executor que detém tokens de registro, material de assinatura e funções na nuvem.
Nas primeiras horas, ninguém sabia dizer o que o binário da fase 2 realmente fazia. Os relatórios da RustSec, StepSecurity e SafeDep documentaram o downloader e os endereços de comando e controle, mas não foram além disso. Essa lacuna já foi preenchida. A Wiz Research analisou as cargas úteis recuperadas e descreve um backdoor bastante capaz. Ele se comunica via HTTPS, envia detalhes do host e credenciais roubadas em formato JSON codificado em base64, lê logins salvos nos bancos de dados SQLite do Chrome, Brave e Edge e instala persistência como um serviço de usuário do systemd no Linux, um LaunchAgent no macOS ou uma chave Run do Registro no Windows. São necessários quatro comandos, um dos quais busca e executa um shell ou PowerShell arbitrário, e o backdoor recorre a um algoritmo de geração de domínios caso o C2 principal fique inativo. Portanto, trate a execução confirmada como um comprometimento do host e presuma que qualquer credencial à qual esse host ou executor tenha acesso possa ter sido comprometida, incluindo senhas salvas no navegador.
A história da infraestrutura também ficou mais interessante. A campanha Mastra, semelhante, realizada em junho, disseminou um infostealer multiplataforma que visava credenciais, dados de navegadores e carteiras de criptomoedas, e a Wiz relata que as cargas úteis arrayref enviam sinais para o mesmo caminho de solicitação C2 usado pela Mastra, que o endereço de sinalização compartilha um emissor de certificado TLS com um host da Mastra e que um C2 relatado pela vítima neste incidente aparece na inteligência de ameaças do Google, relacionada à campanha axios de um agente norte-coreano. A Microsoft atribuiu a atividade da Mastra a um cluster da Coreia do Norte. Trata-se de uma sobreposição documentada de infraestrutura, e não de uma atribuição formal para este incidente, e é muito mais do que apenas duas campanhas compartilhando um provedor de hospedagem.
Por que o modelo de compilação do Rust agrava essa situação
O Rust tem uma sólida reputação em termos de segurança, e essa reputação é merecida nas áreas em que a linguagem realmente atua. A segurança de memória e um sistema de tipos rigoroso não impedem que uma dependência execute deliberadamente código arbitrário antes que seu programa seja encerrado.
O Cargo oferece à dependência duas maneiras de executar código em sua máquina durante a compilação. A primeira é o `build.rs`. Colocar esse arquivo na raiz de um pacote faz com que o Cargo compile o script e o execute “imediatamente antes de compilar o pacote”. Seus propósitos legítimos são reais e comuns, abrangendo a compilação de bibliotecas C, a geração de código e a detecção de recursos — o que é exatamente o que faz com que não se destaque em um diff. A segunda são as macros procedurais, que são compiladas e executadas dentro do compilador enquanto seu código está sendo compilado. A Referência do Rust é direta quanto à consequência: as macros proc “apresentam as mesmas preocupações de segurança que os scripts de compilação do Cargo”. Nenhuma das duas superfícies está isolada em sandbox.
O ecossistema do npm oferece, pelo menos, o comando `npm install –ignore-scripts`, que é a medida de mitigação indicada aqui após o ataque Mastra. O Cargo não possui nenhum equivalente geral, nem na versão estável nem na versão nightly. Não há nenhuma opção de compilação, chave de configuração ou opção `-Z` que simplesmente desative scripts de compilação de dependências arbitrárias e permita que o restante da compilação prossiga, e isolá-los em sandbox é uma questão em aberto no Cargo desde 2018. A única opção de exclusão documentada é restrita. Um pacote que declare uma chave `links` pode ter seu script de compilação substituído na configuração; nesse caso, o script não é compilado nem executado. Isso abrange crates `-sys` que fornecem metadados de bibliotecas C, mas não um crate como o `proc-macro1`. A razão mais profunda para a inexistência de uma opção geral é que um script de compilação não é uma conveniência opcional no momento da instalação, da mesma forma que um gancho `postinstall` do npm. Ele faz parte da compilação, e desativá-lo geralmente significa que o pacote não será compilado.
A distância transitiva também é importante. O `arrayref` é, normalmente, um crate utilitário “folha” que fornece macros para obter referências de array, e suas versões “clean” não declaram dependências de tempo de execução ou de compilação, apenas uma dependência de desenvolvimento do `quickcheck`. Foi isso que tornou a versão 0.3.10 atípica, já que o `proc-macro1` foi a primeira dependência comum que o crate já havia declarado. Seus dependentes diretos incluem tiny-skia-path, tiny-skia, libsecp256k1, multiaddr e schnorrkel, e a verificação de dependências reversas do StepSecurity acrescenta o revm-precompile no lado do Ethereum e o solana-runtime e o spl-token no lado do Solana. Um dos maiores dependentes expostos foi o blake3, com cerca de 166 milhões de downloads no total, que exigia o arrayref ^0.3.5 até a versão 1.8.6 e, em seguida, foi lançado na versão 1.8.7 sem ele às 09h09 UTC na mesma manhã. Acima desses crates, há muito mais projetos que os utilizam. Quase ninguém exposto a esse ataque tinha o arrayref em seu próprio Cargo.toml. Eles tinham uma biblioteca gráfica ou de criptografia, e isso chegava por baixo dela, dois ou três níveis abaixo, em um crate sobre o qual não tinham motivos para se pronunciar.
O aspecto que deve mudar a maneira como você encara o processo de admissão
Todos os controles que, em tese, poderiam ter detectado isso atuam sobre algo diferente do produto final.
A fixação de versão ajudou e é uma proteção real, mas trata-se de uma proteção por inércia. Ela funciona até o momento em que você precisa de uma atualização de segurança; nesse ponto, você resolve o problema e herda o que quer que o registro esteja fornecendo naquele exato momento. A assinatura e a autenticação de dois fatores são controles de publicação valiosos, mas nenhum deles garante que o código que está sendo publicado seja inofensivo. A Equipe de Resposta à Segurança do Rust afirmou que o computador ou as credenciais do mantenedor provavelmente foram comprometidos, sem especificar publicamente qual dos dois, portanto, ainda não é possível afirmar qual controle de publicação teria evitado esse incidente. Uma varredura de análise de composição baseada em CVE não encontrou nenhuma correspondência, pois não havia nenhum CVE registrado durante o período de exposição. Um SBOM poderia ter ajudado, mas apenas como inventário. O proc-macro1 foi inserido no arrayref como uma dependência comum, em vez de uma dependência exclusiva da compilação; portanto, um SBOM compatível com o Cargo ou derivado de um arquivo de bloqueio, gerado antes da compilação, teria listado o proc-macro1 1.0.107 como um componente recém-resolvido. O que ele não teria feito é indicar que esse componente era malicioso. Um nível abaixo, os crates que realmente realizaram o download — base64, rustls e ureq — eram dependências de compilação do proc-macro1, e um SBOM derivado de artefatos ou focado em tempo de execução poderia omitir esses componentes por completo.
A revisão de código-fonte teria falhado no nível de detalhe em que a maioria das equipes realmente opera, pela mesma razão que falhou no ataque ao Mastra. Um revisor que lesse o código da biblioteca do arrayref 0.3.10 encontraria macros limpas e familiares. O comportamento malicioso está um nível abaixo, em um script de compilação pertencente a uma dependência que nenhum membro da equipe escolheu, em um crate cujo código da biblioteca é uma cópia de algo legítimo. Um revisor que lesse o diff completo da versão, incluindo o manifesto, teria percebido uma nova dependência cujo nome diferia em um dígito de “proc-macro2” e teria um ponto de partida. Essa é a profundidade de revisão que vale a pena institucionalizar, e não é a profundidade em que a maioria dos processos de admissão opera.
O que distingue uma posição defensável de uma indefensável é o momento em que a decisão de confiança é tomada. Se deixar para depois que a compilação já tiver começado, provavelmente será tarde demais, pois o pacote já estará no host de compilação e seu script de compilação talvez já tenha sido executado.
Mitigação
Siga estas etapas na ordem, pois as três primeiras determinam se o restante envolverá apenas a limpeza ou a resposta a incidentes.
Encontre os artefatos. Execute a verificação do cache do Cargo descrita acima nas máquinas dos desenvolvedores, nos agentes de compilação e em qualquer imagem de contêiner compilada em 20 de agosto de 2026. Em seguida, pesquise nos arquivos de bloqueio de todos os repositórios por arrayref 0.3.10, append-only-vec 0.1.9, internment 0.8.7 e qualquer ocorrência de proc-macro1, proc-macro-en, aovine, arone, aronenao ou tinymember.
Verifique em relação aos hashes conhecidos. A StepSecurity publicou os valores SHA-256 dos arquivos. O arrayref 0.3.10 é 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae, o proc-macro1 1.0.107 é 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4, e a versão limpa do isca proc-macro1 1.0.106 é b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436. Limite sua pesquisa a todos os três. A versão 1.0.106 não continha carga útil; portanto, encontrá-la não indica execução nem qualquer contato com a infraestrutura do próprio invasor, mas é um indício útil de que algo no ambiente resolveu o typosquat durante sua fase de preparação.
Procure pela execução, não apenas pela presença. Nos terminais, procure por /tmp/rust-setup em hosts Unix e por rust-setup.ps1 ou rust-setup-launch.vbs em %TEMP% no Windows. Esses são os nomes locais que o dropper grava. Na telemetria de proxy, rede ou download, procure separadamente por solicitações com nomes que vão de rust-crate_0.1.0 a rust-crate_0.4.0, que são os nomes de arquivos da fase 2 específicos do alvo que o downloader seleciona no lado remoto, em vez de arquivos que aparecem no disco com esses nomes. Verifique se há wscript.exe sendo gerado a partir de um processo de compilação e se há um processo filho que permaneceu ativo após a etapa de compilação. Um arquivo .crate em cache significa que o arquivo compactado foi baixado. Os artefatos do dropper no disco indicam que o script de compilação foi executado o suficiente para gravar a carga útil, e a telemetria de processo ou de rede é o que confirma que a própria carga útil foi executada. Quando houver suspeita de execução, procure também pela persistência que o backdoor instala, ou seja, um serviço de usuário do systemd não reconhecido no Linux, um LaunchAgent indesejado no macOS ou uma entrada inesperada no HKCU Run no Windows.
Bloqueie e analise o tráfego de saída. Adicione 23.254.165.112 às suas listas de bloqueio, juntamente com 23.254.167.107 e 23.254.167.216 — ambos relatados como endereços de comando e controle da fase 2 — e não limite as regras às portas observadas 9089 e 443. Adicione também hwsrv-798836.hostwindsdns.com e, em seguida, analise retrospectivamente os logs do proxy e do firewall em busca de conexões provenientes de hosts de compilação, incluindo solicitações HTTPS para o caminho /49890878, que é o endpoint de sinalização do estágio 2. Um processo de compilação que acessa um endereço IP simples em uma porta não padrão é um evento de alto risco que merece investigação, independentemente desta campanha.
Analise o que a compilação poderia acessar. Se a carga útil for executada, limite a análise ao que essa identidade possuía, em vez de ao que o crate fez. Em uma máquina de desenvolvedor, isso significa chaves SSH, credenciais de nuvem, tokens de registro, quaisquer segredos expostos por meio do ambiente do shell e os logins salvos no Chrome, Brave ou Edge, já que a carga útil os lê diretamente. Na CI, isso significa segredos do executador, relações de confiança OIDC, tokens de publicação de pacotes e material de assinatura.
Defina corretamente a dependência transitiva. O arrayref 0.3.9 está limpo, não foi removido e está disponível novamente. Quase ninguém tem o arrayref em seu próprio manifesto; portanto, arrayref = “=0.3.9” significa adicionar uma dependência direta que você não usaria de outra forma, e uma definição exata pode tornar o gráfico insatisfável posteriormente. Para uma dependência transitiva, a maneira mais limpa de proceder é usar `cargo update -p arrayref –precise 0.3.9`, com o arquivo de bloqueio resultante confirmado e aplicado.
Torne a disciplina do arquivo de bloqueio obrigatória. Execute a CI com a opção –locked, que instrui o Cargo a falhar em vez de alterar o arquivo de bloqueio; assim, uma resolução inesperada interrompe a compilação em vez de permitir a distribuição. Exija revisão humana em pull requests do bot de dependências que alterem versões transitivas e trate qualquer solicitação de atualização — seja um aviso de “yank”, um pull request do bot ou uma tarefa de CI com status vermelho — como um motivo para ler o diff em vez de refazer a resolução.
Detecção
Observe o que a compilação faz. Os feeds do CVE e os bancos de dados de vulnerabilidades não continham nenhuma informação sobre o proc-macro1 às 07:11 UTC, e os dados de reputação contavam com cinco horas de histórico do registro para análise. O comportamento era o único indício disponível. Uma atividade de rede de saída inesperada proveniente de um script de compilação deve ser tratada como relevante para a segurança, e um processo de compilação que grava um executável em um diretório temporário e o descarta é, por si só, um forte indicador.
Certifique-se de definir corretamente a linhagem do processo ao escrever a regra. Um script de compilação é executado como um executável compilado independente, gerado pelo Cargo — convencionalmente chamado de `build-script-build` no diretório de destino —; portanto, o sinal é enviado a um processo filho do Cargo de curta duração que acessa a rede, e não ao próprio Cargo. Uma carga útil de macro procedural, por outro lado, seria executada dentro do `rustc`. Crie alertas para ambos os casos, bem como para novos executáveis que apareçam em `/tmp` ou `%TEMP%` durante uma compilação.
Em hosts onde haja suspeita de execução, amplie a investigação para além do momento da compilação. O backdoor de estágio 2 persiste por meio de um serviço de usuário do systemd, um LaunchAgent ou uma chave Run do Registro, dependendo da plataforma, e envia sinais via HTTPS; portanto, uma persistência inesperada criada por volta do momento da compilação já constitui, por si só, um motivo para alerta.
Na camada de dependências, fique atento a qualquer nova dependência que apareça em um diff do arquivo de bloqueio, especialmente aquelas cujo nome difira em um caractere ou um dígito de um crate amplamente utilizado. A diferença entre `proc-macro1` e `proc-macro2` é o segredo, e isso fica claro em um diff caso alguém esteja analisando as diferenças de dependências. Observe que os crates de download durante a compilação nesta campanha — base64, rustls e ureq — foram declarados um nível abaixo dentro do proc-macro1; portanto, uma regra que inspecionasse apenas suas próprias dependências diretas de compilação não os teria detectado.
Como o SecureChain para Rust muda o resultado
O motivo pelo qual esse ataque funcionou é que a execução durante a compilação é uma etapa considerada confiável e que não é verificada em praticamente todos os pipelines do Rust. Essa é exatamente a lacuna que o SecureChain para Rust foi desenvolvido para preencher.
O SecureChain recompila todos os pacotes a partir do código-fonte em ambientes fortificados e isolados, além de analisar especificamente o código-fonte dos crates em busca de padrões de ataque durante a compilação, incluindo scripts `build.rs` maliciosos e macros procedurais, bloqueando os crates afetados antes que eles entrem na sua cadeia de ferramentas. Essa distinção é mais relevante aqui do que em um incidente típico do npm. Como não existe no Cargo um equivalente ao –ignore-scripts, uma equipe de Rust não pode desativar scripts de compilação; portanto, a análise precisa ocorrer na fase inicial, em vez de ser desativada dentro do pipeline. Um `build.rs` que recompõe uma URL a partir de fragmentos base64, desativa a validação de certificados e baixa um binário de um endereço IP simples é o tipo de padrão de tempo de compilação que a revisão se propõe a detectar, e revisá-lo antes que o crate chegue ao laptop de um desenvolvedor é muito mais seguro do que deixá-lo ser executado primeiro e investigar depois.
Conheça o SecureChain para Rust →
O SecureChain funciona como uma alternativa verificada e recompilada a partir do código-fonte ao uso direto do crates.io, em vez de um scanner acoplado ao final do pipeline. Os pacotes são analisados em busca de malware conhecido, padrões de código suspeitos e riscos à cadeia de suprimentos, e qualquer item que não passe na inspeção é bloqueado antes de chegar ao seu ambiente. Os riscos específicos do Rust que ele identifica são aqueles evidentes neste incidente, incluindo crates semelhantes, editores comprometidos, versões adulteradas, comportamentos ocultos e apropriação de crates sem manutenção — situação em que a propriedade de um crate abandonado é transferida e, em seguida, usada para distribuir uma atualização maliciosa sob um nome anteriormente confiável. Um crate cujo nome difere em apenas um dígito de uma dependência fundamental é exatamente o padrão de semelhança que um modelo de triagem foi projetado para tratar como suspeito, e esse risco já existia antes de qualquer compilação consumi-lo.
Nada disso altera a forma como os desenvolvedores trabalham. O SecureChain é um substituto direto para repositórios, compatível com o JFrog Artifactory, o Sonatype Nexus e o GitHub Packages, e as equipes mantêm os mesmos pacotes, as mesmas APIs e os mesmos comandos do Cargo. Ele também oferece proveniência SLSA Nível 3, SBOM e saída VEX. Um SBOM faz o inventário dos componentes e das relações de dependência dentro do escopo definido. A proveniência registra como e onde um artefato foi construído e fornece evidências sobre sua origem e processo de construção.
Há um aspecto mais complexo desse problema que o pinning não consegue resolver. O `arrayref` é um crate de utilitários pequeno e estável, do tipo que é escrito uma vez e, em seguida, utilizado discretamente por centenas de dependências ao longo de uma década. Poucas equipes acompanham de perto um crate como esse, e um número muito grande acaba por incorporá-lo em seus projetos. É isso que torna crates como esse alvos atraentes e o que deixa as equipes presas entre uma dependência abandonada e uma reescrita. O SecureChain mantém os componentes suportados monitorados e atualizados de acordo com o SLA enquanto estiverem em cobertura ativa. Quando uma versão atinge o status de fim de vida útil, o Suporte de Ciclo de Vida Ilimitado da TuxCare pode continuar fornecendo correções de segurança para essa versão enquanto você depender dela. Sem atualizações forçadas e sem reescritas.
Por que isso vai além de uma simples campanha
Ecossistema diferente, mesmo erro. A campanha Mastra, em junho, inseriu uma única linha de dependência com typosquatting em mais de 140 pacotes npm limpos. A onda “Mini Shai-Hulud” atingiu 172 pacotes npm e dois pacotes PyPI, embora apresentasse uma proveniência válida de nível 3 do SLSA, e sua continuação, a “Megalodon”, ignorou completamente a publicação dos pacotes, injetando em massa fluxos de trabalho do GitHub Actions em milhares de repositórios para roubar credenciais de nuvem, chaves SSH e tokens. Esta campanha aplica o mesmo esquema no Cargo, utilizando uma conta de mantenedor comprometida, uma dependência com typosquatting e a própria semântica de “yank” do Cargo para criar pressão por atualização. A Wiz documentou, desde então, sobreposições de infraestrutura entre essa campanha e a Mastra, e a Mastra foi atribuída a um cluster norte-coreano; portanto, o ecossistema muda, enquanto o operador pode não mudar.
Em todos esses incidentes, a superfície comprometida variava, seja uma dependência transitiva, um pacote malicioso com proveniência comprovada ou o próprio fluxo de trabalho de CI/CD. A falha comum foi considerar um editor autenticado, uma proveniência válida ou uma compilação aprovada como evidência de que o comportamento ocorrido durante a recepção e a compilação fosse benigno. O Mini Shai-Hulud torna o limite da proveniência concreto, já que seus artefatos maliciosos possuíam uma proveniência SLSA criptograficamente válida. O comportamento deve ser verificado na entrada, antes do início da compilação, e não em uma máquina que já possua suas credenciais.
Conheça o SecureChain para Rust →
Perguntas frequentes
Um arquivo Cargo.lock confirmado me protege?
Para a aquisição da versão maliciosa, em grande parte sim. Um arquivo de bloqueio inalterado mantém um projeto nas versões já resolvidas, o que, para a maioria das equipes, significava o arrayref 0.3.9, inclusive em um contêiner limpo que copia o arquivo de bloqueio para dentro dele. Essa proteção termina no momento em que você resolve novas dependências, seja por meio do `cargo update`, de um bot de dependências, de uma alteração no manifesto que introduza um novo caminho para o crate, de uma compilação executada sem a presença do arquivo de bloqueio ou do `cargo install` sem a opção `–locked`. Seu arquivo de bloqueio também não chega aos seus próprios usuários, já que apenas seu `Cargo.toml` determina o que eles resolvem. E nada disso afeta uma versão maliciosa que já esteja em um cache ou em uma árvore de dependências fornecida.
É possível simplesmente impedir que os scripts de compilação sejam executados?
Não de forma realista. Não existe uma opção do Cargo, seja na versão estável ou na versão noturna, que ignore os scripts de compilação, e a única forma documentada de desativá-los — que consiste em substituir o script de compilação de um pacote que declare uma chave `links` — não é aplicável de maneira geral. Ao contrário dos hooks do `npm install`, os scripts de compilação do Rust fazem parte do processo de compilação e, muitas vezes, são essenciais para produzir um artefato funcional. É por isso que a inspeção precisa ocorrer antes da compilação, em vez de ser desativada durante o processo.
O mantenedor foi o responsável?
A Equipe de Resposta a Segurança do Rust afirmou que não acredita que o autor tenha agido com intenção maliciosa e que a explicação mais provável seja uma máquina comprometida ou credenciais comprometidas. A conta foi bloqueada por precaução. Trata-se da mesma categoria de falha do ataque ao Mastra, em que o ponto de entrada foi a conta npm de um ex-colaborador que permanecia inativa há cerca de 16 meses, sem que seu acesso tivesse sido revogado.
Por que foi importante retirar as versões mais antigas?
A ação de “yank” não exclui uma versão, e os arquivos de bloqueio existentes continuam sendo gerados. O que isso altera é a resolução atual, pois o resolvedor ignora as versões removidas, a menos que elas já estejam no arquivo de bloqueio. A remoção das versões corretas deixou a versão maliciosa como a versão mais recente que a resolução atual aceitaria. O Cargo também exibe um aviso sobre a versão removida assim que se comunica com o índice; portanto, os desenvolvedores que responderam refazendo a resolução com o comando `cargo update` foram os que tiveram maior probabilidade de acabar com a versão 0.3.10.
Será que um SBOM ou um scanner de CVE teria detectado isso?
Não por si só. Não havia nenhum CVE correspondente enquanto as versões maliciosas estavam ativas. Um SBOM gerado antes da compilação poderia ter tornado visível a dependência inesperada do proc-macro1, já que ela foi declarada como uma dependência comum, mas um SBOM é um inventário, e não um detector de malware. Alguns SBOMs também omitem dependências exclusivas da compilação, que era o caso dos crates do downloader dentro do proc-macro1. A detecção ainda exigia uma política ou camada de análise capaz de reconhecer tanto a nova dependência quanto seu comportamento.
O Rust está mais ou menos exposto do que o npm nesse aspecto?
O Rust apresenta riscos de forma diferente. Ele possui menos pacotes e uma superfície de ataque menor em termos de quantidade, e a disciplina do lockfile no desenvolvimento de aplicativos contribui para isso. No entanto, os scripts de compilação e as macros proc são mais difíceis de evitar do que os hooks do `npm install`; portanto, a maneira direta que o npm oferece para suprimir a execução de scripts de ciclo de vida não tem um equivalente direto no Cargo.
Qual é a solução mais importante a longo prazo?
Transfira a decisão de confiança para a fase de admissão. Recompile e inspecione os pacotes antes que eles entrem no seu ambiente, analise as diferenças entre versões, trate qualquer nova dependência como uma alteração relevante para a segurança e exija a proveniência como prova da origem e do processo de compilação, sem considerá-la como evidência de que o comportamento resultante seja inofensivo.