Skip to main content

Por que os lockfiles do npm podem ser um ponto cego de segurança para a injeção de módulos maliciosos

Escrito por

24 de setembro de 2019

0 minutos de leitura

Recentemente, comecei a explorar a ideia de modelar ameaças em pacotes do ecossistema npm. Um incidente como o do event-stream pode acontecer de novo? E outros ataques à cadeia de suprimentos? Qual será o próximo vetor de ataque que ainda não vimos — e será que ele poderia ser totalmente evitado?

Então, um dia, tive uma ideia genial! Deixe-me mostrar como é fácil introduzir backdoors que passam despercebidos pelos responsáveis pelo projeto… deixando seu código vulnerável.

A imagem a seguir mostra como injetei um pacote malicioso por meio de um pull request que fiz em um pacote de código aberto no npm.

Observe com atenção e veja se consegue identificá-lo:

Solicitação de pull request no GitHub mostrando atualizações de versão de dependências no package.json e uma diferença de yarn.lock recolhida

Agora, veja como fica minha lista de verificação para revisar este pull request:

  • Os pacotes que introduzi são conhecidos no ecossistema e estão livres de vulnerabilidades — certo.

  • Não há tentativas de typosquatting nos nomes desses pacotes — certo.

  • Estas são versões válidas desses pacotes e não são maliciosas por si só — certo.

Parece tudo certo. Vamos aprovar e mesclar!

Mas espere. Você revisou o lockfile com atenção? Claro que não — e não culpo você. Isso não é nada óbvio, dadas estas limitações:

  • Por padrão, a interface do GitHub recolhe as diferenças quando elas ultrapassam algumas centenas de linhas de código.

  • Qual é o sentido de revisar este arquivo? Ele é gerado por máquina e não é muito fácil de ler.

Mesmo com essas limitações, o que poderia dar errado, afinal?

Vamos expandir as diferenças e ver o que há dentro:

Solicitação de pull do GitHub mostrando diferenças no yarn.lock, com atualizações de versão das dependências destacadas em vermelho e verde

Quando há um lockfile, seja o yarn.lock do Yarn ou o package-lock.json do npm, a instalação consulta esse arquivo como fonte primária de verdade para as versões dos pacotes e suas origens ao instalar dependências.

Lembra do que eu disse? Uma alteração de 500 linhas em um lockfile é, na verdade, bem pequena. Já vi milhares de linhas de alterações em um package-lock.json do npm ou em um arquivo yarn.lock do Yarn. Quer dizer, quem realmente revisa milhares de linhas de alterações, caractere por caractere? Para piorar, neste lockfile vimos mudanças nas versões das dependências e atualizações de metadados — nada muito fora do comum.

Se rolarmos um pouco mais para baixo para revisar o restante das alterações neste arquivo, encontraremos esta pérola:

Diferenças em uma solicitação de pull do GitHub mostrando alterações nas dependências do yarn.lock, com as linhas removidas destacadas em vermelho e as adicionadas em verde

O que fica claro ao olhar com mais atenção é que substituí o pacote ms original do npm pela minha própria versão, hospedada no meu repositório do GitHub. Eu deveria tê-lo obtido do registro oficial do npm, como estava definido originalmente no lockfile do projeto.

Este projeto usa algumas versões diferentes do semver de ms, mas alterei apenas ms@2.1.1 para usar minha versão personalizada:

yarn list --pattern ms
yarn list v1.17.3
├─ @babel/helper-module-transforms@7.4.4
├─ debug@2.6.9
│  └─ ms@2.0.0
├─ express@4.17.1
│  └─ ms@2.1.1
├─ ms@2.1.2
├─ send@0.16.2
│  └─ ms@2.0.0
├─ serve-static@1.14.1
│  └─ ms@2.1.1
✨  Done in 0.49s.

É muito difícil lembrar de revisar tudo nesse nível de detalhe — não é de se admirar que tão pouca gente confira as alterações de lockfiles caractere por caractere!

Então, o que minha versão de ms@2.1.1 faz?

Quando este pull request for mesclado, vou injetar minha versão maliciosa de ms@2.1.1 no código para controlar seu comportamento durante a execução.

Assim, eu poderia introduzir um backdoor, alterar a lógica do módulo ms ou executar scripts postinstall. Além disso, a URL de hospedagem https://github.com/lirantal/ms/tarball/master é obviamente suspeita neste experimento, mas você perceberia facilmente se eu comprasse um domínio com typosquatting para hospedá-lo em https://registry.npmgs.org/ms/?

Para este experimento, meu pacote hospedado para ms tem um script postinstall simples que exibe um texto e grava em um arquivo. Assim, se você instalasse o próprio pacote, poderia confirmar que o evento post-install é executado durante um yarn install:

"scripts": {
   "postinstall": "echo im installed && echo hello > /tmp/world.txt",
}

Em aplicações baseadas em projetos, a segurança dos lockfiles é fundamental, e eles estão sujeitos a ataques de injeção maliciosa como os descritos neste artigo.

Já os lockfiles usados em pacotes que são bibliotecas servem exclusivamente ao fluxo de desenvolvimento do próprio pacote. Por isso, quando o projeto (pacote) é instalado como dependência em outro projeto, seu lockfile não é usado. Isso significa que, no caso de bibliotecas, a injeção em lockfiles comprometeria principalmente os responsáveis pela manutenção do projeto e seus colaboradores.

O que podemos fazer para aumentar a segurança dos lockfiles?

Como os lockfiles são feitos para serem lidos por máquinas, é fácil processá-los automaticamente. Isso faz deles o alvo perfeito para um linter com políticas predefinidas!

Como contramedida contra esse vetor de ataque, criei o lockfile-lint para ajudar nessa tarefa. Adicionei essa ferramenta como uma etapa rápida da análise estática de código, que você pode executar em um servidor de CI, como o Travis CI ou o Circle CI.

No mínimo, você deve validar se todos os recursos são servidos:

  • por HTTPS

  • de uma fonte confiável

Por exemplo, executar essas verificações na CI de um projeto Yarn é simples:

$ npx lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts yarnpkg.org

Veja um exemplo de erros comuns detectados ao analisar o lockfile de um projeto:

Saída do terminal mostrando o lockfile-lint validando um arquivo yarn.lock, detectando quatro URLs de pacotes que não usam HTTPS e encerrando com o código 1.

Resumo

Os ataques podem assumir muitas formas e tipos diferentes… até mesmo por meio de lockfiles!

Com o incidente do event-stream, vimos o primeiro ataque de engenharia social bem-sucedido contra responsáveis pela manutenção de projetos de código aberto. Talvez tenha sido um dos ataques mais sofisticados à segurança da cadeia de suprimentos de software que vimos até hoje no ecossistema npm e JavaScript.

A injeção em lockfiles pode funcionar de forma semelhante. Por isso, você deve adotar práticas adequadas para mitigar esse problema ou, pelo menos, reduzir o risco. Considere o seguinte:

  • Revise cuidadosamente as alterações nos lockfiles (o lockfile-lint pode ajudar nisso).

  • Permita que apenas responsáveis principais pela manutenção façam alterações em lockfiles, não colaboradores ocasionais.

  • Evite usar lockfiles em bibliotecas.

Se quiser saber mais sobre lockfiles e como são usados, escrevi um artigo detalhado sobre como entender os lockfiles de pacotes no ecossistema npm.

Comece a participar de desafios de Capture the Flag

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