Skip to main content

Por que o incidente com is-promise aconteceu e o que podemos aprender com ele

Escrito por

28 de abril de 2020

0 minutos de leitura

Em 25 de abril de 2020, o desenvolvedor JavaScript e mantenedor Forbes Lindesay lançou a versão 2.2.0 da biblioteca is-promise no npm. Segundo relatos, esse lançamento causou falhas em ferramentas populares de build usadas por desenvolvedores para criar novos projetos, como create-react-app, do Facebook, firebase-tools, do Google, angular-cli, entre outras. Forbes resolveu rapidamente os problemas relacionados à versão 2.2.0 e, cerca de três horas depois, lançou a versão 2.2.2.

De novo, o caso do left-pad? Não exatamente!

Ao contrário do que parece ser a opinião popular entre quem compartilhou essa história, isso não é um problema na cadeia de suprimentos de software e não é parecido com o caso que vimos em 2016, no incidente do left-pad.

Além disso, o problema teve origem em um erro inocente que poderia ter acontecido com qualquer pessoa, e Forbes mostrou ser um mantenedor responsável, corrigindo a falha em poucas horas. Ele também escreveu uma análise pós-incidente, que recomendo muito que você leia.

Neste artigo, quero explicar o que levou ao incidente com is-promise e o que podemos aprender com ele como mantenedores e usuários.

Quem foi afetado pelo incidente com is-promise?

O pacote is-promise do npm é usado por cerca de 500 dependências diretas e tem 12 milhões de downloads por semana. Portanto, é justo dizer que se trata de um pacote popular. No entanto, ele não afetou especificamente milhares de builds de equipes: o impacto foi principalmente sobre quem criou novos projetos usando ferramentas para desenvolvedores.

Ter 500 dependências diretas talvez não pareça algo tão popular. Mas, se você conhece a complexidade do ecossistema npm e suas dependências aninhadas, a estatística do GitHub de 3.500.000 milhões de projetos que dependem de is-promise deve dar uma ideia melhor da popularidade do pacote.

Além disso, o problema só afetaria quem usasse o Node.js 12.16 ou superior — uma versão LTS. No entanto, a maioria dos problemas relatados que vi no GitHub (por exemplo: [1], [2]) menciona usuários do Node.js 13 ou 14, versões que não são estáveis nem recomendadas, a menos que você queira usar as tecnologias mais recentes.

Então, o que aconteceu de fato?

O mantenedor queria adicionar suporte a ES Modules (ESM) para a biblioteca e permitir que ela usasse ao mesmo tempo o sistema de módulos CommonJS (CJS), usado há muito tempo pelo Node.js, e o sistema de módulos JavaScript padronizado, relativamente novo.

Isso aconteceu na primeira versão em cinco anos — a 2.1.0 —, que mostra o diff abaixo. (Veja a comparação original de is-promise no GitHub) Em resumo, as atualizações incluíram:

  • uma exportação default explícita

  • o uso da forma relativamente nova de especificar o sistema de módulos duplo em package.json com a chave exports, além da definição da chave type para ESM

  • atualização das definições do TypeScript

Comparação de código no estilo do GitHub mostrando atualizações nos arquivos JavaScript do is-promise, a mudança da versão 2.1.0 para 2.2.0 e as exportações do módulo.

"Mas o que deu errado, afinal? O campo exports não foi definido corretamente, o que levou à exceção a seguir e fez com que usuários relatassem problemas no GitHub:

Error [ERR_INVALID_PACKAGE_TARGET]: Invalid "exports" main target "index.js" defined in the package config 

O que podemos aprender com esse incidente?

Acredito que há lições para aprendermos tanto como mantenedores quanto como usuários, que podem nos ajudar a nos preparar melhor para futuros incidentes como esse. Na verdade, Forbes já tomou medidas para evitar que problemas desse tipo voltem a acontecer.

Isso é um problema na cadeia de suprimentos de software?

Esse erro honesto poderia ter acontecido com qualquer pessoa. Ele não se deve ao fato de is-promise ser uma biblioteca de uma linha de código, nem tem origem, por si só, em um problema na cadeia de suprimentos de software.

O incidente do left-pad revelou fragilidades na forma como o registro do npm lidava com a propriedade dos pacotes, seu ciclo de vida e outros aspectos. Isso levou à adoção de políticas e medidas para evitar que algo semelhante acontecesse no futuro. O que o registro do npm fez para corrigir o problema com is-promise? Nada — porque não precisava fazer nada. Como já ressaltei, isso não é um problema na cadeia de suprimentos de software.

O versionamento semântico importa

Adicionar ou trocar o suporte de CommonJS para ESM, usando as chaves type e exports em package.json, é algo que, na verdade, exige uma alteração incompatível. No entanto, is-promise@2.2.0 foi publicada como uma alteração menor e acabou sendo resolvida como a versão mais recente para muitos pacotes que dependem dela.

Portanto, nossa segunda lição é pensar com cuidado no versionamento semântico: seu impacto no nosso ecossistema é significativo.

Testes de ponta a ponta do pacote

Uma forma de detectar esse problema antes do lançamento seria adicionar testes de ponta a ponta do pacote, com o objetivo de testá-lo como um consumidor faria e incluir testes básicos para garantir que ele funcione como esperado.

A Forbes incluiu esses testes de ponta a ponta na versão mais recente. Adotei uma abordagem um pouco diferente, mas semelhante em conceito, com testes de ponta a ponta para um pacote chamado Pie My Vulns. Nos meus testes do CircleCI, inicializo o Verdaccio como um registro local de pacotes, publico o pacote e verifico se um npm install, assim como algumas APIs básicas ou comandos de CLI, funcionam como esperado.

Use o Node.js LTS

Você deve sempre usar o Node.js LTS, que, na época em que escrevi este artigo, correspondia à versão 12. Usar versões de ponta do Node.js, como 13 e 14, sem dúvida teria deixado você vulnerável ao problema causado pela alteração no is-promise.

Mas lembre-se de que, neste caso, isso não é totalmente verdade, já que o problema afetou qualquer pessoa que usasse o Node.js 12.16 ou superior, pois essa versão vinha com o suporte experimental a ESM ativado por padrão. Se você ainda usava a versão 12.15 ou anterior, o problema não teria afetado você.

Isso nos traz outra lição importante: confira se todos os seus testes passam nas versões LTS do Node.js. Ainda assim, adicionar suporte a versões posteriores, como o Node.js 14, teria ajudado a detectar o problema neste caso.

Você está atualizando cedo demais?

Se você recebeu uma solicitação automática de pull request para atualizar para a nova versão is-promise@2.2.0 e estava no mesmo cenário de impacto descrito acima, também estava vulnerável ao problema.

Daqui para a frente, como boa prática, não tenha pressa para atualizar: assim, você evita problemas como esse e pacotes maliciosos. É por isso que as atualizações automáticas do Snyk não apressam a atualização das suas versões e esperam 21 dias antes de propor novas versões para seus projetos.

Você sabe como o npx funciona?

Percebi que muitos dos problemas relatados mencionavam o uso do npx e quis esclarecer algumas dúvidas: os arquivos de lock, como package-lock.json ou yarn.lock, não ajudam nesse caso, pois geralmente não são publicados junto com o pacote. Mesmo que fossem, eles não são usados na instalação, que é o que o npx faz como parte da sua execução.

Você pode, no entanto, usar o arquivo de lock shrinkwrap do npm (npm-shrinkwrap.json) para fixar suas dependências, como fiz com is-website-vulnerable. Assim, garanto que sempre envio a mesma árvore — aquela que testo —, independentemente de novas versões serem lançadas no registro.

Se quiser saber mais sobre como funcionam os arquivos de lock, também escrevi sobre como entender os arquivos de lock de pacotes no ecossistema npm.

Resumo

Para concluir, esses problemas acontecem, e devemos agradecer à comunidade por agir e a Forbes Lindesay pelo trabalho para resolver tudo rapidamente. Nós, desenvolvedores e mantenedores, também temos muito a aprender, pois fazemos parte da comunidade open source.

Quer saber mais? Aproveite para ler a análise pós-incidente de Forbes e as lições que ele aprendeu com esse incidente.

A versão corrigida de is-promise para a linha 2.x é a 2.2.2. Também há uma nova versão principal — a 4.0.0 — para quem busca suporte a ES Modules ou TypeScript.

Comece a resolver desafios de capture the flag

Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.