Por que todo mundo deveria usar cooldown de dependências
(blog.yossarian.net)- Cooldown de dependências (dependency cooldown) é uma técnica de segurança simples e eficaz capaz de mitigar a maior parte dos ataques à cadeia de suprimentos de código aberto
- Normalmente, atacantes comprometem projetos populares de código aberto para distribuir código malicioso, mas na maioria dos casos a janela de exposição dura menos de uma semana
- Ao configurar um cooldown para esperar um período após o lançamento de uma nova versão, como 7 dias, é possível reduzir bastante o risco de infecção causado por atualizações automáticas
- Dependabot, Renovate, pnpm e outras ferramentas já oferecem suporte nativo a cooldown, com configuração simples e sem custo adicional
- Se cooldowns forem oferecidos por padrão no nível do gerenciador de pacotes, isso pode fortalecer a segurança da cadeia de suprimentos e reduzir alertas desnecessários
Estrutura dos ataques à cadeia de suprimentos e seus problemas
- A maioria dos ataques à cadeia de suprimentos (supply chain attack) segue o mesmo padrão
- O atacante obtém acesso por meio de roubo de credenciais de um projeto popular de código aberto ou de vulnerabilidades em CI/CD
- Envia alterações maliciosas para canais de distribuição como PyPI, npm etc.
- Usuários instalam a versão infectada por causa de atualizações automáticas ou da falta de fixação de versão
- Um fornecedor de segurança detecta o problema e emite um alerta, depois o repositório de pacotes remove a versão afetada
- O intervalo entre as etapas (1) e (2) costuma ser longo, mas das etapas (2) a (5) tudo acontece em horas ou poucos dias, o que torna curta a janela de ação do atacante
- Janela de oportunidade dos principais casos nos últimos 18 meses
- xz-utils: cerca de 5 semanas
- Ultralytics: 12 horas (etapa 1), 1 hora (etapa 2)
- tj-actions: 3 dias
- chalk: menos de 12 horas
- Nx: 4 horas
- rspack: 1 hora
- num2words: menos de 12 horas
- Kong Ingress Controller: cerca de 10 dias
- web3.js: 5 horas
- Desses casos, 8 tiveram duração de ataque inferior a 1 semana, e a maioria poderia ter sido bloqueada com cooldown
Conceito e efeito do cooldown
- Cooldown é uma forma de adiar o uso de uma nova dependência por um determinado período após sua publicação
- Durante esse período, fornecedores de segurança podem detectar se ela é maliciosa
- Vantagens
- É comprovadamente eficaz e bloqueia a maior parte dos ataques em larga escala
- A implementação é muito simples e, na maioria das ferramentas, pode ser configurada gratuitamente
- Exemplo com Dependabot
version: 2 - package-ecosystem: github-actions directory: / schedule: interval: weekly cooldown: default-days: 7 - Incentiva comportamentos positivos de fornecedores de segurança: em vez de alertas excessivos ou autopromoção, estimula foco em detecção rápida
Conclusão e recomendações
- Em 8 de 10 casos, os ataques duraram uma semana ou menos, e um cooldown de 7 dias seria suficiente para bloquear a maioria
- Com um cooldown de 14 dias, seria possível se defender de todos os casos, exceto xz-utils
- Cooldown não é uma solução perfeita, mas é uma forma simples de reduzir o risco de exposição em 80% a 90%
- Além de Dependabot e Renovate, é necessário melhorar o suporte para que os próprios gerenciadores de pacotes ofereçam cooldown por padrão
- A segurança da cadeia de suprimentos não é apenas um problema técnico, mas também uma questão de estrutura social de confiança; ainda assim, cooldown é uma mitigação prática e útil
3 comentários
Na verdade, se não houver problema, acho que é melhor nem atualizar.
Será mesmo necessário aplicar uma nova versão que não muda muita coisa em relação à anterior, assumindo esse risco?
Comentários do Hacker News
As pessoas se preocupam que, se não atualizarem imediatamente, ficarão expostas a vulnerabilidades graves, mas na prática quase sempre não é assim
Muitos softwares não usam implantação contínua; o cliente instala manualmente novas versões, então as atualizações acontecem em ciclos de semanas ou meses
O importante é fazer monitoramento de dependências e verificar vulnerabilidades divulgadas. Depois de avaliar se o produto é realmente afetado, basta atualizar aquela dependência imediatamente só nesse caso
Existe a percepção de que, saiu versão nova, é preciso atualizar hoje sem falta
Adotar a abordagem de “vamos fazer agora porque depois vai ser mais difícil”, sem revisar as mudanças reais, é ineficiente
Ficar sempre na linha de frente dos números de versão pode até ser contraproducente para a segurança
Na nossa empresa, se o scanner encontra uma vulnerabilidade crítica, temos que atualizar em até 7 dias
Se o prazo estoura, começa um processo complicado por violação de conformidade, então a maioria simplesmente atualiza tudo imediatamente
Apps como navegadores, com muita entrada externa, precisam de atualizações frequentes, mas casos como um app de clima, com entrada limitada, são relativamente mais seguros
Pode ser mais eficiente atualizar periodicamente e combinar isso com medidas de defesa contra ataques à cadeia de suprimentos
O modelo de distribuição no estilo Debian stable, em que a distro gerencia dependências comuns e faz um upgrade geral a cada poucos anos, parece cada vez mais sensato
Alguns ecossistemas se movem rápido demais ou têm empacotamento ruim por distribuição
Por exemplo, ainda é difícil instalar bibliotecas Node.js via apt e usá-las em um projeto
Um ecossistema que se move rápido sem mudanças fundamentais não é saudável
JS quase não teve progresso real nos últimos 3 anos, mas para recompilar um projeto de 3 anos atrás você enfrenta uma dor no nível de um rewrite
Em distros como Arch, às vezes nem isso existe
Há a hipótese de que, para prevenir ataques à cadeia de suprimentos, é melhor impor um período de cooldown nas atualizações de dependências do que usar imediatamente a versão mais nova para tentar bloquear 0-days
A comparação é entre a probabilidade de a atualização introduzir uma nova vulnerabilidade e a probabilidade de corrigir uma já existente
Pelo SemVer, versões de patch são relativamente seguras, então dá até para aplicar cooldowns mais curtos nesse caso
Por exemplo, se você está em 2.3.4 e sai a 2.4.0, pode ser melhor esperar pela 2.4.1 se não houver nenhum recurso urgente
A maioria das vulnerabilidades não vem de ataques intencionais, mas de bugs comuns
Uma abordagem ainda mais forte é ter políticas para limitar o número e a complexidade das dependências
Em vez de bibliotecas que “fazem de tudo”, só deveriam ser adicionadas dependências pequenas e com propósito claro
Além disso, se as bibliotecas oferecerem versões LTS com apenas patches de segurança, a gestão fica mais fácil
Reimplementar por conta própria pode ser desperdício. Fiquei curioso sobre exemplos concretos dessas “everything libraries” problemáticas
Se você confia no mesmo desenvolvedor em várias bibliotecas, a relação de confiança importa mais do que a quantidade de pacotes
Complexidade tem correlação com vulnerabilidades, mas não é a causa direta
Agora já dá para fazer escolhas pensando na manutenção de longo prazo, e não só na velocidade de curto prazo
No mundo C++, isso às vezes até vira diferencial competitivo
A pressão para atualizar sempre para a versão mais recente vem da crença equivocada de que o software está sempre melhorando
Na prática, isso pode apenas trocar bugs conhecidos por novos bugs desconhecidos
Monitorar issues públicas e aplicar patch só quando necessário é algo razoável
Ou seja, foi um caso em que a versão antiga acabou sendo mais segura
Talvez também porque já estejamos usando softwares suficientemente estáveis
Se todo mundo adotar a postura de “vamos esperar um pouco”, no fim ninguém valida primeiro, como numa tragédia dos comuns
Quanto mais se espera, mais dívida técnica se acumula, então são necessárias medidas de mitigação como atualizações graduais e zero trust + monitoramento
Nesse meio-tempo, scanners de segurança já detectam vulnerabilidades
Se um invasor publica um release não autorizado, em alguns casos isso é percebido rapidamente
A ideia de cooldown é boa, mas existe o risco de o atacante explorar isso para criar uma falsa urgência
Pode divulgar um “patch de segurança urgente” para induzir instalação antecipada, quando na verdade aquela versão é maliciosa
É preciso se preparar para esse tipo de ataque de pressão psicológica
Outro motivo para o cooldown é dar aos mantenedores tempo para perceber por conta própria que foram comprometidos
Atacantes miram momentos em que o mantenedor está ausente, como férias, conferências ou feriados
Muitos projetos são mantidos por uma ou duas pessoas, então essa defasagem de tempo é muito importante
Depende das características do projeto e da superfície de ataque
A era de simplesmente seguir “boas práticas” deveria acabar
Segurança é como um esporte de contato: é preciso pensar criticamente nos detalhes todos os dias
Também existe o limite do cooldown como ideia de que o tempo, por si só, torna algo seguro
Se ninguém olhar o código, o risco continua igual mesmo depois de uma semana
Em vez disso, um modelo de implantação gradual (gradual rollout) pode ser eficaz
Cada consumidor define um fator de atraso, de modo que quem aceita mais risco encontra os problemas primeiro,
enquanto os demais ficam protegidos nesse intervalo
Atualizações perigosas são removidas da fila, e consumidores com atraso maior nem chegam a vê-las
Ultimamente, às vezes fico na dúvida sobre o que é mais administrável: o esforço de simplesmente reinventar a roda ou o esforço de gerenciar um Tetris de dependências.
Se algo estiver errado por um tempo, basta corrigir meu próprio código, mas no Tetris de dependências é difícil até depurar qual roda saiu do eixo de repente.