YAML a l'air aimable et il mord
Publié le 12/08/2025 · 18 min de lecture · Outils pour développeurs
Daniel Okonkwo — Développeur front-end et rédacteur Tech chez OneKitly
Performance web · Formats de fichiers
Vérifié à partir de 6 sources
YAML 1.2 déclare JSON comme un sous-ensemble : tout document JSON est donc du YAML valide. Ce que YAML ajoute par-dessus, c'est une étape de résolution qui devine un type pour chaque scalaire non quoté, et cette devinette a changé entre les versions de la spécification. Sous YAML 1.1, les jetons y, yes, no, on et off se résolvent en booléens : c'est le fameux problème norvégien, où le code pays NO devient false. YAML 1.2 les a retirés du schéma de base, si bien qu'un analyseur 1.2 les laisse en chaînes. Le comportement que tu obtiens dépend entièrement de ton analyseur, pas de ton fichier. Mesuré sur un même document : js-yaml 4.3.0, qui se décrit comme un analyseur YAML 1.2, renvoie la chaîne "no" ; PyYAML 6.0.3, un analyseur YAML 1.1, renvoie False. La même fracture frappe 01234, qui vaut 1234 en 1.2 et l'octal 668 en 1.1, et 12:30:00, chaîne en 1.2 et entier 45000 en 1.1. Certains pièges ont survécu intacts au changement de version : 1.10 est le flottant 1,1 dans les deux, donc un numéro de version perd silencieusement un chiffre. Ajoute une indentation significative où les tabulations sont franchement interdites, deux styles de scalaires en bloc avec trois modes de troncature, et des ancres qui font passer 403 octets à 39 Mo : la règle pratique s'écrit d'elle-même — quote toute chaîne qui pourrait se lire autrement.
YAML, c'est JSON plus une couche d'inférence de types, et c'est l'inférence qui est dangereuse. Le même fichier passé dans un analyseur YAML 1.2 puis 1.1 : no est une chaîne dans l'un et false dans l'autre, 01234 vaut 1234 dans l'un et 668 dans l'autre, et 12:30:00 est un nombre dans l'un des deux.
Un sur-ensemble de JSON, plus une idée dangereuse
YAML 1.2 énonce la relation explicitement : JSON est un sous-ensemble de YAML, et un processeur YAML conforme accepte n'importe quel document JSON. Exécute-le et l'affirmation tient — donner {"a": 1, "b": [1,2,3]} à js-yaml renvoie exactement l'objet attendu. Tout ce que l'article précédent disait des types manquants de JSON s'applique donc ici aussi, inchangé. YAML ne te donne pas un type entier distinct d'un flottant, ni un type binaire, ni un schéma.
Ce que YAML ajoute, c'est du confort pour les humains : commentaires, pas de guillemets sur les clés, pas de guillemets sur la plupart des chaînes, pas d'accolades, pas de virgules, du texte en bloc qui conserve ses retours à la ligne, et un mécanisme de référence permettant d'écrire une valeur une fois et de la réutiliser. Ce sont de vraies améliorations pour un fichier maintenu par une personne, et c'est pourquoi YAML pilote la configuration de la plupart des outils de déploiement en usage aujourd'hui.
L'idée dangereuse est celle qui rend tout cela possible. Si les clés et les chaînes n'ont pas besoin de guillemets, l'analyseur doit décider de ce que signifie un jeton non quoté, et YAML appelle cette étape la résolution : un scalaire simple est confronté à un ensemble d'expressions régulières et se voit attribuer un type. C'est de là que vient chaque surprise de cet article, et c'est aussi la partie du langage qui a changé entre YAML 1.1 et YAML 1.2 — un même fichier peut donc vouloir dire deux choses différentes selon la bibliothèque qui le lit.
Le problème norvégien, et la version de spécification que tu as réellement
YAML 1.1 définissait un type booléen au jeu de jetons généreux : true et false, mais aussi yes et no, on et off, et dans le dépôt de types les lettres seules y et n. Un fichier listant des codes pays transforme donc la Norvège en booléen, car le code ISO de la Norvège est NO. Voilà tout le fameux bogue, et ce n'est pas un défaut d'analyseur : c'est la spécification qui fonctionne comme elle est écrite.
YAML 1.2 a corrigé cela en rétrécissant le schéma de base. La balise booléenne ne correspond plus qu'à true et false, en quelques casses. Tout le reste demeure une chaîne. Mais ton fichier ne porte pas de version, et presque personne n'écrit la directive %YAML qui en déclarerait une : la version qui s'applique est donc celle qu'implémente ta bibliothèque — et les deux sont encore largement en production aujourd'hui.
Vérifie donc, ne suppose pas. Le chemin YAML de ce site utilise js-yaml 4.3.0, dont la description du paquet indique « YAML 1.2 parser and serializer », et passer le document de test dedans renvoie la chaîne "no" pour le cas norvégien — sous ses quatre schémas, du failsafe au défaut. Le même document dans PyYAML 6.0.3, un analyseur YAML 1.1, renvoie le False de Python. Mêmes octets, sens opposés, et aucune des deux bibliothèques n'a tort. Prends trois minutes et passe ton propre fichier dans ton propre analyseur avant de croire quoi que ce soit d'écrit là-dessus en ligne, y compris ceci.
Des nombres qui ne sont pas ceux que tu as tapés
Le plus coûteux a survécu intact au changement de version. Écris version: 1.10 et les deux analyseurs renvoient le nombre flottant 1,1 — js-yaml donne 1.1, PyYAML donne 1.1 en flottant. YAML l'a résolu en nombre, et un nombre n'a pas de zéro final : ta version 1.10 est désormais la version 1.1 et se classe avant 1.2 et 1.9. Quote-la et elle survit : "1.10" revient en chaîne 1.10 dans les deux. Les numéros de version, de pièce, de modèle et tout ce qui a un dernier chiffre signifiant doivent être quotés, dans toutes les versions de la spécification.
Les zéros initiaux sont pires, car les deux versions divergent sur la manière. Écris zip: 01234 et js-yaml renvoie le nombre 1234 — le zéro initial a purement disparu, car le motif entier de YAML 1.2 est un décimal ordinaire. PyYAML renvoie 668, car sous YAML 1.1 un zéro initial signifie octal, et 1234 lu en base 8 vaut 668. Un code postal, un code guichet ou un numéro de compte écrit sans guillemets est donc corrompu par les deux analyseurs, en deux valeurs fausses différentes. Dans l'autre sens, la notation octale propre à YAML 1.2, 0o17, se résout en 15 sous js-yaml et reste la chaîne "0o17" sous PyYAML, qui ne reconnaît pas la forme récente.
Le dernier piège numérique est le sexagésimal, mort avec YAML 1.1. Cette version résolvait les groupes de chiffres séparés par des deux-points en entiers de base 60 : 12:30:00 devenait 45000 et 22:22 devenait 1342. Exécute : PyYAML renvoie exactement ces deux entiers, et js-yaml renvoie les chaînes. Un horaire à la crontab, une durée, un fragment d'adresse MAC ou un repère temporel musical écrit sans guillemets dans un fichier 1.1 devient un entier sans rapport évident avec ce que tu as écrit — 45000 est le nombre de secondes dans douze heures et demie, ce qui est au moins logique, et 1342 vaut 22 fois 60 plus 22, ce que personne n'a voulu dire.
Espaces, tabulations interdites et les deux scalaires en bloc
L'indentation est la structure en YAML : les espaces ne sont donc pas cosmétiques et un formateur ne peut pas les reformater librement. La spécification interdit purement les tabulations dans l'indentation — pas déconseille, interdit — car une tabulation n'a pas de largeur définie et l'analyseur ne pourrait pas savoir à quelle profondeur tu voulais être. Donne une liste indentée par tabulations à js-yaml et il s'arrête sur une plainte précise : tab characters must not be used in indentation, ligne 2 colonne 1. C'est l'échec YAML le plus courant dans un éditeur qui convertit obligeamment les espaces initiaux.
Le texte multiligne utilise l'un des deux styles en bloc, et la différence porte exactement sur ce qu'il advient de tes sauts de ligne. Le style littéral, écrit avec une barre verticale, conserve chaque saut : un bloc de deux lignes revient en "line one\nline two\n". Le style plié, écrit avec un signe supérieur, joint les lignes consécutives par une espace : le même bloc revient en "line one line two\n". Le style plié conserve tout de même une ligne vide comme un vrai saut, si bien qu'un bloc plié de deux paragraphes renvoie "para one line a para one line b\npara two\n" — un paragraphe joint, puis une véritable coupure.
Par-dessus le style vient un indicateur de troncature qui décide du saut de ligne final, et c'est la partie qu'on oublie. Le défaut, écrit sans rien de plus, est le mode clip : exactement un saut final est conservé. Un signe moins le supprime, si bien que le même bloc revient en "line one\nline two" sans aucun saut final. Un signe plus conserve toutes les lignes vides finales : un bloc suivi d'une ligne vide renvoie "line one\nline two\n\n". Cela compte plus qu'il n'y paraît : un certificat, une clé SSH ou un script shell inséré dans un fichier de configuration a généralement besoin de son saut final, tandis qu'un jeton ou un mot de passe inséré ne doit généralement pas en avoir. Se tromper produit une non-correspondance sur une valeur qui paraît identique dans tous les diffs.
Ancres et alias : une vraie fonctionnalité qui est aussi une bombe
Une ancre nomme un nœud avec une esperluette, un alias y renvoie avec un astérisque, et la clé de fusion tire les clés d'une table dans une autre. Ensemble, elles suppriment la principale source de dérive en configuration : écris tes valeurs par défaut une fois, puis surcharge les deux valeurs qui diffèrent par environnement. Exécute : cela fait exactement ce que tu veux — un bloc de base avec un délai de 30 et trois tentatives, fusionné dans dev avec le délai porté à 5, donne à dev un délai de 5 et trois tentatives, tandis que prod garde 30 et 3.
Une subtilité mérite d'être connue avant de s'y fier : un alias ne copie pas, il partage. Charge un document où deux entrées de liste font alias vers la même ancre, et les deux entrées sont le même objet — l'égalité stricte entre elles est vraie, et l'égalité stricte avec l'original aussi. Mute l'une après le chargement et tu les as toutes mutées. C'est sans conséquence pour de la configuration en lecture seule, et c'est un vrai piège dans du code qui normalise ou corrige l'arbre chargé sur place.
Ce même partage rend possible l'attaque par expansion. Enchaîne des ancres de sorte que chaque niveau soit une liste de neuf références au niveau inférieur : la taille du document logique vaut neuf à la puissance de la profondeur tandis que le fichier reste minuscule. Mesuré avec js-yaml : quatre niveaux font 241 octets de YAML et 54 127 octets de JSON, soit un facteur 225 ; six niveaux font 349 octets et 4,38 Mo, un facteur 12 563 ; sept niveaux font 403 octets et 39,46 Mo, un facteur 97 914. À neuf niveaux, le nombre de nœuds logiques atteint 387 420 489. Note où tombe le coût réel : js-yaml a analysé tout cela en moins de deux millisecondes, car les alias sont des références partagées et le graphe en mémoire reste petit. C'est la sérialisation du résultat qui a pris 363 millisecondes à sept niveaux. La défense n'est donc pas seulement une limite d'analyseur : c'est le refus de parcourir, copier profondément ou sérialiser un arbre issu d'un YAML non fiable, plus un plafond de taille sur l'entrée et une limite d'expansion d'alias si ta bibliothèque en propose une.
La règle, et ce qu'un formateur peut ou non faire pour toi
Quote toute chaîne qui pourrait se lire autrement. En pratique, la liste est courte et mémorisable : tout ce qui est ou contient yes, no, on, off, y, n, true ou false ; tout code pays, en particulier NO ; toute valeur à zéro initial ; tout numéro de version ou de pièce à zéro final après une virgule décimale ; tout ce qui contient des deux-points, comme une heure ou une durée ; les mots null et none et le tilde ; et tout ce qui ressemble à un nombre mais est en réalité un identifiant. Les guillemets simples sont la forme la plus sûre, car à l'intérieur rien n'est un échappement : un chemin Windows ou une expression régulière passe intact.
Une chose mérite d'être claire, car c'est une idée fausse répandue. Le formateur YAML de ce site n'analyse pas le YAML. Il normalise le texte — il convertit les tabulations en deux espaces, supprime les espaces en fin de ligne, réduit les suites de lignes vides et, pour la minification, retire les commentaires et les lignes vides — et il vérifie séparément la seule erreur dure, une tabulation dans l'indentation. Il ne résout jamais un scalaire : il ne peut donc pas transformer ton NO en false ni ton 1.10 en 1,1, et il ne reformatera pas tes scalaires en bloc. C'est délibéré : un formateur qui ferait faire un aller-retour à ton fichier dans un analyseur appliquerait silencieusement la version des règles de résolution de cet analyseur et te rendrait un document différent.
Pour la même raison, traite toute conversion YAML vers JSON comme une étape avec perte et inspecte le résultat. La conversion est précisément le moment où les règles de résolution s'appliquent : c'est donc aussi le moyen le moins cher de découvrir ce que ton analyseur pense réellement de ton fichier — donne-lui ta configuration, lis le JSON, et chaque bogue de quoting de cet article devient visible en une passe.
| Écrit dans le fichier | js-yaml 4.3.0 (YAML 1.2) | PyYAML 6.0.3 (YAML 1.1) | Forme sûre |
|---|---|---|---|
| no | "no" (chaîne) | False (booléen) | 'no' |
| NO (le code ISO de la Norvège) | "NO" (chaîne) | False (booléen) | 'NO' |
| yes | "yes" (chaîne) | True (booléen) | 'yes' ou true |
| 1.10 (un numéro de version) | 1,1 (nombre — le zéro a disparu) | 1,1 (flottant — le zéro a disparu) | "1.10" |
| 01234 (un code postal) | 1234 (nombre — décimal) | 668 (entier — lu en octal) | "01234" |
| 12:30:00 (une heure) | "12:30:00" (chaîne) | 45000 (entier — base 60) | "12:30:00" |
| 0o17 (notation octale YAML 1.2) | 15 (nombre) | "0o17" (chaîne — forme inconnue en 1.1) | Écris plutôt la valeur décimale |
Questions fréquentes
- Le problème norvégien est-il corrigé, et comment savoir quelle version implémente mon analyseur ?
- Il est corrigé dans la spécification et pas nécessairement dans ton programme. YAML 1.2 a retiré yes, no, on et off de la balise booléenne du schéma de base : un analyseur 1.2 les laisse donc en chaînes. YAML 1.1 les résolvait tous, ainsi que les lettres seules y et n de son dépôt de types, d'où le code pays ISO NO devenu false. Ton fichier ne déclare pas de version — la directive %YAML existe mais presque personne ne l'écrit — donc le comportement vient entièrement de la bibliothèque. Le test fiable prend une minute : charge un document de deux lignes contenant une clé dont la valeur simple est no, et affiche le type du résultat. Mesuré ici, js-yaml 4.3.0 renvoie la chaîne "no", et ce sous ses quatre schémas, du failsafe jusqu'au défaut. PyYAML 6.0.3 renvoie le False de Python. Les deux sont des implémentations correctes de versions différentes de la spécification. Note aussi que les implémentations divergent sur les formes d'une lettre même à l'intérieur de 1.1 — PyYAML laisse un y nu et un n nu en chaînes — donc tester vaut mieux que lire. Et quelle que soit la réponse, quoter la valeur est gratuit et fonctionne dans toutes les versions.
- Pourquoi mon numéro de version 1.10 est-il devenu 1,1 ?
- Parce qu'il correspondait au motif flottant, et un flottant n'a aucune mémoire des zéros finaux. Les deux analyseurs sont d'accord ici — js-yaml renvoie 1.1 et PyYAML renvoie 1.1 en flottant — ce n'est donc pas une question de version de spécification et le quoting est le seul correctif. Les dégâts dépassent le simple affichage. Le tri casse, car en tant que nombre 1,1 se place entre 1,09 et 1,2 alors qu'en tant que chaîne de version 1.10 vient après 1.9. L'égalité casse, car une recherche de la version nommée 1.10 ne trouve plus la clé. Et re-sérialiser le fichier réécrit 1.1 sur le disque : l'erreur devient permanente dans ton dépôt et le diff montre une modification d'un caractère qui semble plausible. Le même piège attrape tout identifiant pointé à deux composants : un numéro de chapitre, une révision de micrologiciel, une version de schéma, un code produit décimal. Écris "1.10" entre guillemets. S'il te faut une vraie sémantique d'ordre, utilise une version sémantique à trois composants : elle contient deux points et ne peut donc pas correspondre au motif flottant — 1.10.0 est une chaîne dans tous les analyseurs sans guillemets, même si les mettre quand même ne coûte rien et évite d'avoir à y penser.
- Quand utiliser la barre verticale et quand le signe supérieur ?
- Utilise la barre verticale, le style littéral, dès que les sauts de ligne font partie de la valeur : script shell, certificat, clé SSH, instruction SQL, fichier de configuration imbriqué, diagramme ASCII. Mesuré, un bloc littéral de deux lignes renvoie "line one\nline two\n" — chaque saut préservé, plus un à la fin. Utilise le signe supérieur, le style plié, pour de la prose que tu veux couper dans le fichier source mais stocker sur une seule ligne : longue description, message d'aide, gabarit de commit. Le même bloc plié renvoie "line one line two\n" — le saut interne est devenu une espace. Le style plié respecte tout de même les lignes vides comme des séparations de paragraphes : un bloc plié avec une ligne vide au milieu renvoie "para one line a para one line b\npara two\n". Choisis ensuite l'indicateur de troncature délibérément. La forme nue conserve exactement un saut final, un signe moins le supprime entièrement, un signe plus les conserve tous. Un certificat PEM a besoin de son saut final : la forme nue convient. Un jeton ou un secret d'une ligne ne doit pas en avoir : utilise le moins. C'est ce détail qui produit la mystérieuse non-correspondance de signature ou l'erreur d'analyse openssl sur une valeur qui paraît correcte dans le fichier.
- Les ancres et alias sont-ils sûrs en configuration de production ?
- Dans les fichiers que tu écris et relises, oui — ce sont les bons outils pour des valeurs par défaut partagées, et la clé de fusion produit exactement le motif de surcharge par environnement dont la plupart des déploiements ont besoin. Deux réserves s'appliquent même là. Un alias partage le nœud au lieu de le copier, vérifié ici par l'égalité stricte entre deux entrées aliasées : tout code qui mute l'arbre chargé sur place changera toutes les occurrences d'un coup. Et la clé de fusion est une fonctionnalité de YAML 1.1 reprise par convention plutôt qu'une partie du schéma de base 1.2 : la prise en charge varie donc selon la bibliothèque — vérifie la vôtre avant d'en dépendre. Dans les fichiers venus de l'extérieur de ton organisation, traite les alias comme un vecteur d'épuisement de ressources. Un éventail de neuf sur sept niveaux faisait ici 403 octets d'entrée et 39,46 Mo de sortie, un facteur 97 914, et neuf niveaux atteindraient 387 420 489 nœuds logiques. Savoir où tombe le coût est utile : js-yaml a analysé chacun de ces cas en moins de deux millisecondes, car les alias restent des références partagées ; la facture de 363 millisecondes est arrivée à la sérialisation du résultat. La défense est donc un plafond de taille sur l'entrée, une limite d'expansion d'alias si ta bibliothèque en expose une, et une règle interdisant de copier profondément ou de sérialiser un arbre chargé depuis du YAML non fiable.
- Le formateur YAML de ce site change-t-il le sens de mes valeurs ?
- Non, car il ne les analyse jamais. Il travaille au niveau du texte : il remplace les tabulations par deux espaces, supprime les espaces en fin de ligne, réduit les suites de trois lignes vides ou plus à une seule, et en mode minification retire les lignes de commentaire et les lignes vides. Il exécute aussi une validation, la seule erreur dure que la spécification définit pour les espaces, et signale le numéro de ligne de toute tabulation trouvée dans l'indentation. Comme aucun scalaire n'est jamais résolu, un NO nu reste les deux caractères NO, 1.10 garde son zéro final, et tes scalaires en bloc reviennent exactement tels qu'écrits. C'est un choix de conception délibéré : un formateur bâti sur un analyseur ferait passer ton fichier par les règles de résolution de cet analyseur et te rendrait un document aux valeurs différentes, ce qui est précisément la panne dont parle cet article. Si tu veux voir comment ton fichier se résout, convertis-le plutôt en JSON — c'est l'étape où la résolution se produit, et lire le JSON est l'audit le plus rapide de ton quoting.
Articles qui pourraient t'intéresser
Tous les guides →Outils similaires
Sources
- YAML.org — YAML Ain't Markup Language (YAML) version 1.2 — core schema, block scalars, anchors and aliases
- YAML.org — YAML 1.1 specification and type repository (the bool, int and sexagesimal resolutions)
- Ecma International — ECMA-404: The JSON Data Interchange Syntax — the subset YAML 1.2 accepts
- nodeca — js-yaml — implementation used by this site; its package metadata declares a YAML 1.2 parser and serializer
- PyYAML — PyYAML documentation — a YAML 1.1 implementation, used here as the 1.1 reference
- OWASP — XML External Entity and billion-laughs style entity-expansion guidance, the same class of attack as YAML alias expansion
Tu as repéré une erreur dans cet article ?