15 pontos por GN⁺ 2025-11-22 | 3 comentários | Compartilhar no WhatsApp
  • 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

 
regentag 2025-11-23

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?

 
GN⁺ 2025-11-22
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

    • Falta essa cultura de atualização seletiva em todo o ecossistema
      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
    • O problema prático é que muitas equipes de segurança de grandes empresas impõem uma política de “zero CVE”
      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
    • As pessoas temem 0-days, mas a maior parte dos problemas reais vem de vulnerabilidades sem patch por centenas de dias
    • O ponto central é se o app recebe entrada desconhecida vinda de fora
      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
    • A estratégia de “atualizar só quando necessário” parece boa na teoria, mas na prática o custo de avaliar cada vulnerabilidade no contexto do produto é alto demais
      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

    • Não se deve confundir “movimento” com “ação”
      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
    • Os resultados de busca de pacotes node no Debian mostram que alguns existem, mas não de forma completa
      Em distros como Arch, às vezes nem isso existe
    • Com Rust, dá para trabalhar bem só com o repositório do Debian
    • O modelo de versões estáveis traz o peso de o app ter que suportar simultaneamente versões antigas e novas
  • 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

    • 0-days divulgados publicamente vêm com um aviso de segurança (advisory), então ferramentas como Dependabot ignoram o cooldown e aplicam o patch imediatamente
      A maioria das vulnerabilidades não vem de ataques intencionais, mas de bugs comuns
    • O padrão sempre se baseia em hipóteses. Quando surgir informação nova, ele pode ser mudado
  • 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

    • Esse argumento aparece com frequência, mas na prática reutilizar código já validado é muito mais eficiente
      Reimplementar por conta própria pode ser desperdício. Fiquei curioso sobre exemplos concretos dessas “everything libraries” problemáticas
    • A maior parte dos ataques à cadeia de suprimentos acontece na superfície de engenharia social
      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
    • Com o surgimento de ferramentas de IA, caiu o custo de implementar por conta própria dependências não essenciais
      Agora já dá para fazer escolhas pensando na manutenção de longo prazo, e não só na velocidade de curto prazo
    • Dependências excessivamente fragmentadas podem, ao contrário, aumentar a quantidade e o peso do build
    • É difícil justificar que bibliotecas de baixo nível tragam outras dependências
      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

    • No caso do log4shell, por exemplo, o log4j 1.x não era vulnerável, e o bug foi introduzido na linha 2.x
      Ou seja, foi um caso em que a versão antiga acabou sendo mais segura
    • Antes, os intervalos entre releases eram maiores e a melhora de qualidade era mais clara; hoje, a maioria das mudanças é periférica
      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

    • Esperar cerca de uma semana depois de uma nova versão sair não gera uma dívida técnica relevante
      Nesse meio-tempo, scanners de segurança já detectam vulnerabilidades
    • Os ataques recentes à cadeia de suprimentos foram detectados não por exposição do consumidor, mas por dar tempo de reação aos mantenedores
    • Mesmo que o número de consumidores diminua, pesquisadores e mantenedores ganham tempo para analisar
      Se um invasor publica um release não autorizado, em alguns casos isso é percebido rapidamente
    • Sistemas de grande porte já fazem rollout gradual de qualquer forma, então atualização total imediata nem é viável
  • 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

    • Se o atacante distribuir o exploit junto com uma correção de bug, pode induzir os desenvolvedores a quebrar o cooldown e instalar
    • Surge a pergunta: “como se cria esse tipo de ruído?” — isto é, como um atacante manipularia a opinião ao redor disso
  • 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

 
aer0700 2025-11-23

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.