1 pontos por GN⁺ 4 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • 35,8% dos CVEs divulgados pela EEF CNA são de consumo não controlado de recursos e, no ecossistema BEAM, o esgotamento recorrente de atoms representa uma parcela significativa
  • Esgotamento de atoms é uma vulnerabilidade de negação de serviço: atoms não passam por garbage collection, acumulam-se em uma tabela global e, quando ela enche, a VM trava
  • Criar atoms a partir de dados cujo conjunto possível de valores não é garantidamente finito, como entrada do usuário, cria risco de DoS, e isso também vale para o scheme de URI
  • O risco existe não só em chamadas explícitas como binary_to_atom/1 e String.to_atom/1, mas também na decodificação de chaves JSON em atoms e na geração dinâmica baseada em interpolação de strings
  • O tratamento seguro é evitar criar novos atoms em tempo de execução e, para valores conhecidos, limitar-se a tabelas de consulta explícitas ou variantes da família to_existing_atom, além de verificar com linters

Vulnerabilidades de negação de serviço causadas por esgotamento de atoms

  • Entre os CVEs divulgados pela EEF CNA, 35,8% são de consumo não controlado de recursos e, no ecossistema BEAM, o problema recorrente de esgotamento de atoms representa uma parcela significativa {p:36}
  • A distribuição atual pode ser consultada na página Common Weaknesses da EEF CNA
  • Esgotamento de atoms é uma vulnerabilidade de negação de serviço (DoS)
    • Atoms não passam por garbage collection
    • São armazenados em uma tabela global de atoms
    • Quando a tabela enche, a VM trava
  • Criar atoms a partir de valores não finitos, especialmente entrada do usuário, torna-se um DoS em potencial
  • O risco não se limita a chamadas óbvias
    • Em Erlang, binary_to_atom/1, list_to_atom/1
    • Em Elixir, String.to_atom/1, List.to_atom/1
  • Também existem padrões de risco menos visíveis
    • Geração dinâmica de atoms via interpolação em Erlang:
      % Erlang: geração dinâmica de atom via interpolação
      list_to_atom("field_" ++ UserInput)
      
    • Decodificação de chaves JSON como atoms em Elixir:
      
      
      
      # Elixir: decodificar JSON com chaves atom
      Jason.decode(json, keys: :atoms)
      
    • Geração dinâmica de atoms via interpolação em Elixir:
      
      
      
      # Elixir: geração dinâmica de atom via interpolação
      :"field_#{user_input}"
      

Formas seguras de tratamento e o que verificar

  • Vulnerabilidades de esgotamento de atoms não surgem apenas por descuido; elas aparecem com frequência em código que pressupõe que a entrada é controlada ou finita
  • O scheme de URI é um exemplo representativo
    • Pode parecer que há apenas alguns schemes a tratar
    • Se o valor vem de entrada externa, já não é possível garantir que o conjunto de valores seja finito
  • Código que cria atoms a partir de entradas não é seguro a menos que o conjunto de valores possíveis seja finito, conhecido e imposto
  • A abordagem mais segura é não criar novos atoms em tempo de execução
  • Se os valores permitidos forem conhecidos, é mais seguro usar uma tabela de consulta explícita
    % Erlang
    case Scheme of
        <<"http">> -> http;
        <<"https">> -> https;
        _ -> error
    end
    
  • Quando uma tabela de consulta não for prática, deve-se usar variantes que só operam com atoms já existentes, sem criar novos atoms
    • Essas funções não criam novos atoms e geram erro
    % Erlang
    binary_to_existing_atom(Value)
    list_to_existing_atom(Value)
    
    
    
    
    # Elixir
    String.to_existing_atom(value)
    List.to_existing_atom(value)
    
  • Linters ajudam a detectar padrões de risco antes que virem vulnerabilidades
    • Em projetos Elixir, vale considerar ativar Credo.Check.Warning.UnsafeToAtom do Credo
    • Essa verificação sinaliza usos inseguros de String.to_atom/1, List.to_atom/1, Module.concat/1,2 e Jason.decode/2 com keys: :atoms
    • Essa verificação vem desativada por padrão
  • Mantenedores de projetos Erlang ou Elixir devem procurar no código por criação de atoms a partir de binários, strings, chaves JSON, componentes de URI, cabeçalhos e valores de configuração
  • Essa categoria de vulnerabilidade é uma das mais fáceis de corrigir antes de virar um CVE
  • Orientações mais detalhadas estão reunidas no guia de prevenção de esgotamento de atoms do EEF Security Working Group

1 comentários

 
GN⁺ 4 시간 전
Comentários do Lobste.rs
  • Parece semelhante à situação do Symbol em Ruby antes de ele se tornar coletável pelo garbage collector

  • Não entendi o título. Isso com certeza parece um footgun

    • Acho que a ideia do título é que chamar a exaustão de atom de “apenas um footgun” subestima a gravidade do problema
    • Se bem me lembro, embora eu não use Erlang no dia a dia, atoms não são coletados pelo garbage collector
      Se você pensar “Ruby também não tem symbols parecidos com os atoms de Erlang?”, está certo, mas Ruby coleta symbols com garbage collector
      Além disso, por padrão, a tabela de consulta onde os atoms de Erlang são armazenados permite no máximo 1.048.576 itens
      Se atoms forem criados dinamicamente a partir de entrada do usuário, isso é muito perigoso e expõe o software a ataques de negação de serviço
    • Entendi como “é um problema maior do que um simples footgun”
      Dito isso, pela minha experiência, o próprio termo “footgun” já é bem amplo, então de qualquer forma a formulação do título soa estranha
    • Sim, isso também parece um footgun gigantesco
  • Fiquei surpreso porque soa como se alguma parte fundamental do design ou da implementação fosse ruim. É ainda mais inesperado numa linguagem que vive sendo elogiada na internet

    • Um atom no BEAM é essencialmente uma string internada e tem uma tabela global de bytes↔inteiros
      Adicionar contagem de referências a essa tabela teria um custo alto e mudaria as características de escalabilidade de código que existe há décadas
      O número máximo de atoms é de 1 milhão por padrão e é definido no momento em que a VM inicia
      É uma armadilha, mas não é difícil de evitar. Há muito tempo a recomendação é: “não crie atoms a partir de entrada do usuário”
      Por exemplo, ao fazer parse de JSON, normalmente você não converte as chaves em atoms, ou só converte quando o atom já existe. Assim, você pode fazer pattern matching com chaves atom, esses atoms já terão sido criados pelo carregamento do código, e cláusulas genéricas podem receber strings em vez de atoms
    • Também é preciso considerar que Erlang era uma linguagem de nicho usada por programadores profissionais em casos especiais
      Elixir é muito mais popular, então um desenvolvedor Erlang provavelmente sabe disso, mas um desenvolvedor Elixir talvez não saiba
  • Pessoalmente, acho estranho usar atoms dessa forma. Eu entendo os atoms de Erlang mais ou menos como o tipo enum em C
    Vejo isso como um recurso de conveniência em que, ao escrever uma palavra de certo jeito, ela internamente vira um enum
    O texto menciona entrada do usuário, mas nem entendo por que existiria um caso de uso em que alguém quisesse criar novos enums a partir de entrada do usuário. Parece algo de uso extremamente limitado
    Os comentários ao lado falam de parsing, mas, no cenário ideal, não seria um parse de uma estrutura de dados previamente conhecida? Sinto que estou deixando passar alguma coisa

    • Fugindo um pouco da pergunta, em K os symbols são internados globalmente e, como em Erlang, é possível matar um processo K ao esgotar a tabela de symbols
      Nessa linguagem, há pelo menos duas vantagens em ter symbol como um tipo separado, e não apenas como comparação rápida de igualdade. Um symbol é atômico, isto é, não é uma sequência em forma de lista de caracteres, então vários operadores o tratam de forma diferente; além disso, symbols são vetorizados e podem ser armazenados de forma compacta em listas homogêneas
      Em K e Q, é muito desejável representar colunas de tabelas de banco de dados com tipos vetorizados. Isso melhora a localidade, usa memória com mais eficiência e oferece muitos caminhos rápidos para vários operadores. Mas, por causa das restrições da tabela de symbols, é preciso ter cuidado ao usar symbol em colunas de alta cardinalidade
      Se você estiver fazendo parse de JSON com esquema conhecido, symbols são ótimos como chaves de dicionário e, em k2/k3, são praticamente indispensáveis. Mas, se for um JSON de origem desconhecida, eles não devem vir da entrada do usuário
      Em alguns dialetos de K, o comprimento do symbol é limitado para que ele possa ser empacotado e transportado como um valor de 64 bits. Em troca de abrir mão de generalidade, isso elimina a necessidade da própria tabela de symbols
  • Essa distinção entre “entrada controlada” e “entrada não controlada” me parece, em segurança, algo como verificar se é null ou não
    Quando vejo itens dizendo que webpack-plugin-less-css sofre negação de serviço ao receber um arquivo CSS não confiável, sinto bastante fadiga de CVE
    Mesmo assim, seria bom haver uma marcação melhor de fronteiras aqui. Por exemplo, seria útil conseguir lidar bem também com regras de composição, como quais propriedades de segurança são preservadas ao passar por concatenação de strings
    E, se você pegou um monte de coisas recebidas via HTTP POST e marcou tudo como SafeString, aí isso é responsabilidade sua até certo ponto