- 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/1ouString.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
- En Erlang :
- 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}"
- Création dynamique d’atom via interpolation en Erlang :
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,2et deJason.decode/2aveckeys: :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
Avis sur Lobste.rs
Ça ressemble à la situation avant que les
Symbolde Ruby ne deviennent ramassés par le garbage collectorJe ne comprends pas le titre. Ça ressemble clairement à un footgun
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
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
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
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
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
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
nullQuand je vois des entrées du type «
webpack-plugin-less-cssprovoque un déni de service s’il reçoit un fichier CSS non fiable », ça me donne une vraie fatigue CVECela 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é