1 points par GN⁺ 4 시간 전 | 1 commentaires | Partager sur WhatsApp
  • 35,8 % des CVE publiées par EEF CNA relèvent d’une consommation de ressources non contrôlée, et dans l’écosystème BEAM, les cas répétés d’épuisement des atoms en représentent une part importante
  • L’épuisement des atoms est une vulnérabilité de déni de service : les atoms ne sont pas ramassés par le garbage collector, s’accumulent dans une table globale, et lorsque cette table est pleine, la VM plante
  • Créer des atoms à partir de données dont l’ensemble des valeurs possibles n’est pas garanti comme fini, comme les entrées utilisateur, introduit un risque de DoS, et le schéma d’URI ne fait pas exception
  • Le risque existe non seulement dans des appels explicites comme binary_to_atom/1 ou String.to_atom/1, mais aussi dans le décodage de clés JSON en atoms et la génération dynamique basée sur l’interpolation de chaînes
  • Pour traiter cela en toute sécurité, il faut éviter de créer de nouveaux atoms à l’exécution, limiter les valeurs connues à des tables de correspondance explicites ou à la famille to_existing_atom, et vérifier le code avec un linter

Vulnérabilités de déni de service causées par l’épuisement des atoms

  • Parmi les CVE publiées par EEF CNA, 35,8 % concernent une consommation de ressources non contrôlée, et dans l’écosystème BEAM, les problèmes récurrents d’épuisement des atoms en représentent une part importante {p:36}
  • La répartition actuelle peut être consultée sur la page Common Weaknesses de l’EEF CNA
  • L’épuisement des atoms est une vulnérabilité de déni de service (DoS)
    • Les atoms ne sont pas ramassés par le garbage collector
    • Ils sont stockés dans une table globale des atoms
    • Lorsque la table est pleine, la VM plante
  • Créer des atoms à partir de valeurs non finies, en particulier d’entrées utilisateur, devient une source potentielle de DoS
  • Le risque ne se limite pas aux appels les plus évidents
    • En Erlang : binary_to_atom/1, list_to_atom/1
    • En Elixir : String.to_atom/1, List.to_atom/1
  • Il existe aussi des motifs de risque moins visibles
    • Création dynamique d’atom via interpolation en Erlang :
      % Erlang: 보간을 통한 동적 atom 생성
      list_to_atom("field_" ++ UserInput)
      
    • Décodage de clés JSON en atoms en Elixir :
      
      
      
      # Elixir: JSON을 atom 키로 디코딩
      Jason.decode(json, keys: :atoms)
      
    • Création dynamique d’atom via interpolation en Elixir :
      
      
      
      # Elixir: 보간을 통한 동적 atom 생성
      :"field_#{user_input}"
      

Méthodes de traitement sûres et points à vérifier

  • Les vulnérabilités d’épuisement des atoms ne viennent pas seulement d’une simple négligence : elles apparaissent souvent dans du code qui suppose que les entrées sont contrôlées ou finies
  • Le schéma d’URI en est un exemple typique
    • On peut avoir l’impression qu’il n’y a que quelques schémas à traiter
    • Mais si la valeur vient d’une entrée externe, il n’est plus garanti que l’ensemble des possibilités soit fini
  • Le code qui crée des atoms à partir d’entrées n’est sûr que si l’ensemble des valeurs possibles est fini, connu et effectivement imposé
  • L’approche la plus sûre consiste à ne pas créer de nouveaux atoms à l’exécution
  • Si les valeurs autorisées sont connues, il est plus sûr d’utiliser une table de correspondance explicite
    % Erlang
    case Scheme of
        <<"http">> -> http;
        <<"https">> -> https;
        _ -> error
    end
    
  • Lorsqu’une table de correspondance n’est pas pratique, il faut utiliser des variantes qui n’emploient que des atoms existants sans en créer de nouveaux
    • Ces fonctions ne créent pas de nouveaux atoms et lèvent une erreur
    % Erlang
    binary_to_existing_atom(Value)
    list_to_existing_atom(Value)
    
    
    
    
    # Elixir
    String.to_existing_atom(value)
    List.to_existing_atom(value)
    
  • Un linter peut aider à repérer les motifs de risque avant qu’ils ne deviennent une vulnérabilité
    • Dans les projets Elixir, il peut être utile d’activer Credo.Check.Warning.UnsafeToAtom de Credo
    • Cette vérification signale les usages non sûrs de String.to_atom/1, List.to_atom/1, Module.concat/1,2 et de Jason.decode/2 avec keys: :atoms
    • Cette vérification est désactivée par défaut
  • Les mainteneurs de projets Erlang ou Elixir devraient rechercher le code qui crée des atoms à partir de binaires, de chaînes, de clés JSON, de composants d’URI, d’en-têtes ou de valeurs de configuration
  • Cette catégorie de vulnérabilités fait partie de celles qu’il est relativement facile de corriger avant qu’elles ne deviennent des CVE
  • Des recommandations plus détaillées sont rassemblées dans le guide de prévention de l’épuisement des atoms du Security Working Group de l’EEF

1 commentaires

 
GN⁺ 4 시간 전
Avis sur Lobste.rs
  • Ça ressemble à la situation avant que les Symbol de Ruby ne deviennent ramassés par le garbage collector

  • Je ne comprends pas le titre. Ça ressemble clairement à un footgun

    • L’idée du titre semble être que qualifier l’épuisement des atoms de « simple footgun » sous-estime la gravité du problème
    • Si je me souviens bien, même si je n’utilise pas Erlang tous les jours, les atoms ne sont pas ramassés par le garbage collector
      Si vous vous dites « Ruby n’a pas des symbols comme les atoms d’Erlang ? », oui, mais Ruby ramasse ses symbols via le garbage collector
      En plus, la table de lookup qui stocke les atoms Erlang n’autorise par défaut qu’un maximum de 1 048 576 entrées
      Créer dynamiquement des atoms à partir d’entrées utilisateur, comme dans un formulaire, est très dangereux et expose le logiciel à des attaques par déni de service
    • Je l’ai compris comme voulant dire que c’est un problème plus grave qu’un « simple » footgun
      Cela dit, d’après mon expérience, le terme « footgun » est déjà assez large, donc dans tous les cas le titre reste maladroit
    • Oui, ça ressemble aussi à un énorme footgun
  • Je suis surpris, car on dirait qu’une partie fondamentale de la conception ou de l’implémentation est mauvaise. C’est encore plus inattendu pour un langage qui n’a cessé d’être encensé sur Internet

    • Les atoms du BEAM sont essentiellement des chaînes internées et utilisent une table globale octets↔entiers
      Ajouter un comptage de références à cette table aurait un coût élevé et changerait les propriétés de montée en charge d’un code vieux de plusieurs décennies
      Le nombre maximal d’atoms est de 1 million par défaut, et il est fixé au démarrage de la VM
      C’est bien un piège, mais pas difficile à éviter. La recommandation de longue date est de « ne pas créer d’atoms à partir d’entrées utilisateur »
      Par exemple, si vous parsez du JSON, on évite en général de convertir les clés en atoms, ou on ne les convertit que si l’atom existe déjà. Cela permet de faire du pattern matching sur des clés atomiques, ces atoms étant déjà créés lors du chargement du code, tandis qu’une clause de secours peut recevoir une chaîne à la place d’un atom
    • Il faut garder à l’esprit qu’Erlang était un langage de niche utilisé par des programmeurs spécialisés dans des cas particuliers
      Elixir est beaucoup plus grand public : un développeur Erlang a de fortes chances de connaître ce point, alors qu’un développeur Elixir peut très bien l’ignorer
  • Personnellement, je trouve déjà étrange d’utiliser les atoms de cette manière. Je les comprends grosso modo comme l’équivalent d’un type enum en C
    J’y vois une fonctionnalité pratique qui permet qu’un mot saisi d’une certaine façon devienne en interne un enum
    L’article mentionne des entrées utilisateur, mais je ne vois même pas dans quels cas d’usage on voudrait créer de nouveaux types enum à partir d’entrées utilisateur. L’usage me paraît extrêmement limité
    Les commentaires à côté parlent de parsing, mais idéalement on parse une structure de données connue à l’avance, non ? J’ai l’impression de passer à côté de quelque chose

    • C’est un peu à côté de la question, mais dans K, les symbols sont internés globalement et, comme avec Erlang, on peut tuer un processus K en épuisant la table des symbols
      Dans ce langage, il y a au moins deux avantages à faire des symbols un type distinct plutôt qu’un simple mécanisme d’égalité rapide. Un symbol est un atom, c’est-à-dire une unité atomique et non une séquence de caractères sous forme de liste, donc de nombreux opérateurs le traitent différemment, et les symbols sont vectorisés, ce qui permet de les stocker de manière compacte dans des listes homogènes
      Dans K et Q, il est très souhaitable de représenter les colonnes des tables de base de données avec des types vectorisés. Cela améliore la localité, utilise la mémoire plus efficacement et offre de nombreux chemins rapides pour divers opérateurs. Mais à cause des contraintes de la table des symbols, il faut faire attention lorsqu’on utilise des symbols pour des colonnes à forte cardinalité
      Si vous parsez du JSON avec un schéma connu, les symbols sont d’excellentes clés de dictionnaire et, en k2/k3, ils sont pratiquement indispensables. Mais si le JSON est de provenance inconnue, il ne doit pas venir d’une entrée utilisateur
      Dans certains dialectes de K, la longueur des symbols est volontairement limitée pour pouvoir les empaqueter et les déplacer comme des valeurs 64 bits. On perd en généralité, mais on supprime du même coup le besoin d’une table des symbols
  • La distinction entre « entrée contrôlée » et « entrée non contrôlée » me fait penser, en sécurité, à quelque chose du même genre que la présence ou non de null
    Quand je vois des entrées du type « webpack-plugin-less-css provoque un déni de service s’il reçoit un fichier CSS non fiable », ça me donne une vraie fatigue CVE
    Cela dit, j’aimerais qu’on dispose ici de meilleurs marqueurs de frontière. Par exemple, ce serait bien de mieux traiter aussi les règles de composition, comme les propriétés de sécurité qui restent garanties après une concaténation de chaînes
    Et si vous avez pris tout ce que vous recevez en HTTP POST pour le marquer en SafeString, là c’est quand même en partie votre responsabilité