YAML parece amable y muerde
Publicado el 12/8/2025 · 18 min de lectura · Herramientas para desarrolladores
Daniel Okonkwo — Desarrollador front-end y redactor de Tecnología en OneKitly
Rendimiento web · Formatos de archivo
Verificado con 6 fuentes
YAML 1.2 declara a JSON un subconjunto, así que todo documento JSON es YAML válido. Lo que YAML añade encima es un paso de resolución que adivina un tipo para cada escalar sin comillas, y esa adivinanza cambió entre versiones de la especificación. Bajo YAML 1.1 los tokens y, yes, no, on y off se resuelven a booleanos: es el famoso problema noruego, donde el código de país NO se vuelve false. YAML 1.2 los quitó del esquema básico, así que un analizador 1.2 los deja como cadenas. Qué comportamiento obtienes depende enteramente de tu analizador, no de tu fichero. Medido sobre un mismo documento: js-yaml 4.3.0, que se describe como analizador YAML 1.2, devuelve la cadena "no"; PyYAML 6.0.3, un analizador YAML 1.1, devuelve False. La misma fractura golpea a 01234, que es 1234 en 1.2 y el octal 668 en 1.1, y a 12:30:00, cadena en 1.2 y entero 45000 en 1.1. Algunos peligros sobrevivieron intactos al cambio de versión: 1.10 es el flotante 1,1 en ambos, así que un número de versión pierde un dígito en silencio. Añade indentación significativa donde las tabulaciones están terminantemente prohibidas, dos estilos de escalar en bloque con tres modos de recorte, y anclas que llevan 403 bytes a 39 MB: la regla práctica se escribe sola — entrecomilla toda cadena que pueda leerse como otra cosa.
YAML es JSON más una capa de inferencia de tipos, y la inferencia es la parte peligrosa. El mismo fichero pasado por un analizador YAML 1.2 y por uno 1.1: no es una cadena en uno y false en el otro, 01234 es 1234 en uno y 668 en el otro, y 12:30:00 es un número en uno de los dos.
Un superconjunto de JSON, más una idea peligrosa
YAML 1.2 enuncia la relación explícitamente: JSON es un subconjunto de YAML, y un procesador YAML conforme acepta cualquier documento JSON. Ejecútalo y la afirmación se sostiene — dar {"a": 1, "b": [1,2,3]} a js-yaml devuelve exactamente el objeto esperado. Así que todo lo que el artículo anterior decía de los tipos que faltan en JSON se aplica aquí también, sin cambios. YAML no te da un tipo entero distinto de un flotante, ni un tipo binario, ni un esquema.
Lo que YAML añade es comodidad para humanos: comentarios, sin comillas en las claves, sin comillas en la mayoría de cadenas, sin llaves, sin comas, texto en bloque que conserva sus saltos de línea, y un mecanismo de referencia para escribir un valor una vez y reutilizarlo. Son mejoras reales para un fichero que mantiene una persona, y por eso YAML gobierna la configuración de la mayor parte del utillaje de despliegue en uso hoy.
La idea peligrosa es la que hace posible todo eso. Si las claves y las cadenas no necesitan comillas, el analizador tiene que decidir qué significa un token sin comillas, y YAML llama a ese paso resolución: un escalar simple se compara con un conjunto de expresiones regulares y se le asigna un tipo. De ahí viene cada sorpresa de este artículo, y es también la parte del lenguaje que cambió entre YAML 1.1 y YAML 1.2 — así que el mismo fichero puede significar dos cosas distintas según la biblioteca que lo lea.
El problema noruego, y qué versión de la especificación tienes en realidad
YAML 1.1 definía un tipo booleano de repertorio generoso: true y false, pero también yes y no, on y off, y en el repositorio de tipos las letras sueltas y y n. Un fichero que lista códigos de país convierte pues a Noruega en booleano, porque el código ISO de Noruega es NO. Ese es todo el famoso fallo, y no es un defecto del analizador: es la especificación funcionando como está escrita.
YAML 1.2 lo arregló encogiendo el esquema básico. La etiqueta booleana solo casa ya con true y false, en unas pocas capitalizaciones. Todo lo demás sigue siendo una cadena. Pero tu fichero no lleva versión, y casi nadie escribe la directiva %YAML que declararía una, así que la versión que se aplica es la que implemente tu biblioteca — y ambas siguen ampliamente en producción hoy.
Comprueba, pues, no supongas. La ruta YAML de este sitio usa js-yaml 4.3.0, cuya descripción de paquete reza «YAML 1.2 parser and serializer», y pasar el documento de prueba por ella devuelve la cadena "no" para el caso noruego — bajo sus cuatro esquemas, del failsafe al predeterminado. El mismo documento por PyYAML 6.0.3, un analizador YAML 1.1, devuelve el False de Python. Los mismos bytes, sentidos opuestos, y ninguna de las dos bibliotecas se equivoca. Tómate tres minutos y pasa tu propio fichero por tu propio analizador antes de creerte nada escrito sobre esto en línea, incluido esto.
Números que no son los que tecleaste
El más caro sobrevivió intacto al cambio de versión. Escribe version: 1.10 y ambos analizadores devuelven el número en coma flotante 1,1 — js-yaml da 1.1, PyYAML da 1.1 como flotante. YAML lo resolvió como número, y un número no tiene cero final: tu versión 1.10 es ahora la versión 1.1 y se ordena antes que 1.2 y 1.9. Entrecomíllala y sobrevive: "1.10" vuelve como la cadena 1.10 en ambos. Los números de versión, de pieza, de modelo y todo lo que tenga un último dígito con significado hay que entrecomillarlos, en toda versión de la especificación.
Los ceros iniciales son peores, porque las dos versiones discrepan en el cómo. Escribe zip: 01234 y js-yaml devuelve el número 1234 — el cero inicial simplemente desapareció, porque el patrón entero de YAML 1.2 es un decimal corriente. PyYAML devuelve 668, porque bajo YAML 1.1 un cero inicial significa octal, y 1234 leído en base 8 vale 668. Así que un código postal, un código de sucursal o un número de cuenta escrito sin comillas queda corrompido por ambos analizadores, en dos valores erróneos distintos. En sentido contrario, la notación octal propia de YAML 1.2, 0o17, se resuelve a 15 en js-yaml y sigue siendo la cadena "0o17" en PyYAML, que no reconoce la forma reciente.
La última trampa numérica es la sexagesimal, y murió con YAML 1.1. Aquella versión resolvía los grupos de dígitos separados por dos puntos como enteros en base 60: 12:30:00 se volvía 45000 y 22:22 se volvía 1342. Ejecuta: PyYAML devuelve exactamente esos dos enteros, y js-yaml devuelve las cadenas. Un horario tipo crontab, una duración, un fragmento de dirección MAC o una marca temporal musical escrita sin comillas en un fichero 1.1 se convierte en un entero sin relación evidente con lo que escribiste — 45000 es el número de segundos de doce horas y media, lo que al menos es lógico, y 1342 es 22 por 60 más 22, que no es lo que nadie quiso decir.
Espacios, tabulaciones prohibidas y los dos escalares en bloque
La indentación es estructura en YAML, lo que significa que los espacios no son cosméticos y un formateador no puede recolocarlos libremente. La especificación prohíbe por completo las tabulaciones en la indentación — no desaconseja, prohíbe — porque una tabulación no tiene anchura definida y el analizador no tendría manera de saber a qué profundidad querías estar. Dale una lista indentada con tabulaciones a js-yaml y se detiene con una queja precisa: tab characters must not be used in indentation, línea 2 columna 1. Ese error es el fallo YAML más común en un editor que convierte servicialmente los espacios iniciales.
El texto multilínea usa uno de dos estilos en bloque, y la diferencia es exactamente qué ocurre con tus saltos de línea. El estilo literal, escrito con una barra vertical, conserva cada salto: un bloque de dos líneas vuelve como "line one\nline two\n". El estilo plegado, escrito con un signo mayor que, une las líneas consecutivas con un espacio: el mismo bloque vuelve como "line one line two\n". El plegado sí conserva una línea en blanco como salto real, así que un bloque plegado de dos párrafos devuelve "para one line a para one line b\npara two\n" — un párrafo unido y luego una ruptura genuina.
Encima del estilo hay un indicador de recorte que decide el salto de línea final, y es la parte que se olvida. El valor por defecto, escrito sin nada más, es clip: se conserva exactamente un salto final. Un signo menos lo elimina, así que el mismo bloque vuelve como "line one\nline two" sin salto final alguno. Un signo más conserva todas las líneas en blanco finales: un bloque seguido de una línea vacía devuelve "line one\nline two\n\n". Esto importa más de lo que parece: un certificado, una clave SSH o un script de shell incrustado en un fichero de configuración suele necesitar su salto final, mientras que un token o una contraseña incrustada normalmente no debe tenerlo. Equivocarse produce un desajuste sobre un valor que parece idéntico en todos los diffs.
Anclas y alias: una función real que también es una bomba
Un ancla nombra un nodo con un ampersand, un alias remite a él con un asterisco, y la clave de fusión trae las claves de un mapa a otro. Juntas eliminan la mayor fuente de deriva en configuración: escribe tus valores por defecto una vez y luego sobrescribe los dos que difieren por entorno. Ejecútalo y hace exactamente lo que quieres — un bloque base con un tiempo de espera de 30 y tres reintentos, fusionado en dev con el tiempo llevado a 5, da a dev un tiempo de 5 y tres reintentos, mientras prod conserva 30 y 3.
Hay una sutileza que conviene conocer antes de confiar en ello: un alias no copia, comparte. Carga un documento donde dos entradas de lista aliasen el mismo ancla y las dos entradas son el mismo objeto — la igualdad estricta entre ellas es cierta, y también con el original. Muta una tras la carga y las has mutado todas. Esto es inocuo para configuración de solo lectura y una trampa real en código que normaliza o parchea el árbol cargado en el sitio.
Esa misma compartición es lo que hace posible el ataque por expansión. Encadena anclas de modo que cada nivel sea una lista de nueve referencias al nivel inferior: el tamaño del documento lógico es nueve elevado a la profundidad mientras el fichero sigue siendo minúsculo. Medido con js-yaml: cuatro niveles son 241 bytes de YAML y 54 127 bytes de JSON, un factor de 225; seis niveles son 349 bytes y 4,38 MB, un factor de 12 563; siete niveles son 403 bytes y 39,46 MB, un factor de 97 914. A nueve niveles el recuento de nodos lógicos llega a 387 420 489. Fíjate en dónde cae el coste real: js-yaml analizó todo esto en menos de dos milisegundos, porque los alias son referencias compartidas y el grafo en memoria sigue siendo pequeño. Fue serializar el resultado lo que tardó 363 milisegundos a siete niveles. Así que la defensa no es solo un límite del analizador: es negarse a recorrer, copiar en profundidad o serializar un árbol procedente de YAML no confiable, más un tope de tamaño en la entrada y un límite de expansión de alias si tu biblioteca lo ofrece.
La regla, y qué puede y qué no puede hacer un formateador por ti
Entrecomilla toda cadena que pueda leerse como otra cosa. En la práctica es una lista corta y memorizable: todo lo que sea o contenga yes, no, on, off, y, n, true o false; todo código de país, en especial NO; todo valor con cero inicial; todo número de versión o de pieza con cero final tras una coma decimal; todo lo que lleve dos puntos, como una hora o una duración; las palabras null y none y la tilde; y todo lo que parezca un número pero sea en realidad un identificador. Las comillas simples son la forma más segura, porque dentro de ellas nada es un escape: una ruta de Windows o una expresión regular pasa intacta.
Una cosa conviene dejarla clara, porque es un malentendido común. El formateador YAML de este sitio no analiza YAML. Normaliza el texto — convierte las tabulaciones en dos espacios, quita los espacios finales, reduce las series de líneas en blanco y, para minificar, elimina comentarios y líneas vacías — y comprueba aparte el único error duro, una tabulación en la indentación. Nunca resuelve un escalar, así que no puede convertir tu NO en false ni tu 1.10 en 1,1, y no reformateará tus escalares en bloque. Es deliberado: un formateador que hiciera pasar tu fichero por un analizador aplicaría en silencio la versión de las reglas de resolución de ese analizador y te devolvería un documento distinto.
Por la misma razón, trata cualquier conversión de YAML a JSON como un paso con pérdida e inspecciona el resultado. Convertir es exactamente el momento en que se disparan las reglas de resolución, así que es también la forma más barata de averiguar qué piensa de verdad tu analizador que dice tu fichero — dale tu configuración, lee el JSON, y cada fallo de entrecomillado de este artículo se hace visible en una pasada.
| Escrito en el fichero | js-yaml 4.3.0 (YAML 1.2) | PyYAML 6.0.3 (YAML 1.1) | Forma segura |
|---|---|---|---|
| no | "no" (cadena) | False (booleano) | 'no' |
| NO (el código ISO de Noruega) | "NO" (cadena) | False (booleano) | 'NO' |
| yes | "yes" (cadena) | True (booleano) | 'yes' o true |
| 1.10 (un número de versión) | 1,1 (número — el cero desapareció) | 1,1 (flotante — el cero desapareció) | "1.10" |
| 01234 (un código postal) | 1234 (número — decimal) | 668 (entero — leído en octal) | "01234" |
| 12:30:00 (una hora del día) | "12:30:00" (cadena) | 45000 (entero — base 60) | "12:30:00" |
| 0o17 (notación octal de YAML 1.2) | 15 (número) | "0o17" (cadena — forma desconocida en 1.1) | Escribe el valor decimal en su lugar |
Preguntas frecuentes
- ¿Está arreglado el problema noruego, y cómo sé qué versión implementa mi analizador?
- Está arreglado en la especificación y no necesariamente en tu programa. YAML 1.2 quitó yes, no, on y off de la etiqueta booleana del esquema básico, así que un analizador 1.2 los deja como cadenas. YAML 1.1 los resolvía todos, más las letras sueltas y y n de su repositorio de tipos, de ahí que el código de país ISO NO se volviera false. Tu fichero no declara versión — la directiva %YAML existe pero prácticamente nadie la escribe — así que el comportamiento viene enteramente de la biblioteca. La prueba fiable lleva un minuto: carga un documento de dos líneas con una clave cuyo valor simple sea no, e imprime el tipo del resultado. Medido aquí, js-yaml 4.3.0 devuelve la cadena "no", y lo hace bajo los cuatro esquemas que trae, del failsafe hasta el predeterminado. PyYAML 6.0.3 devuelve el False de Python. Ambas son implementaciones correctas de versiones distintas de la especificación. Ten en cuenta también que las implementaciones difieren en las formas de una letra incluso dentro de 1.1 — PyYAML deja una y suelta y una n suelta como cadenas — así que probar gana a leer. Y sea cual sea la respuesta, entrecomillar el valor es gratis y funciona en todas las versiones.
- ¿Por qué mi número de versión 1.10 se convirtió en 1,1?
- Porque casó con el patrón de flotante, y un flotante no tiene memoria de los ceros finales. Ambos analizadores coinciden aquí — js-yaml devuelve 1.1 y PyYAML devuelve 1.1 como flotante — así que esto no es una cuestión de versión de la especificación y entrecomillar es el único arreglo. El daño va más allá de un fallo de presentación. La ordenación se rompe, porque como número 1,1 queda entre 1,09 y 1,2 mientras que como cadena de versión 1.10 va después de 1.9. La igualdad se rompe, porque una búsqueda de la versión llamada 1.10 ya no encuentra la clave. Y reserializar el fichero escribe 1.1 de vuelta al disco: el error se vuelve permanente en tu repositorio y el diff muestra un cambio de un carácter con pinta plausible. La misma trampa atrapa cualquier identificador con punto de dos componentes: un número de capítulo, una revisión de firmware, una versión de esquema, un código de producto decimal. Escríbelo como "1.10" entre comillas. Si necesitas semántica de orden real, usa una versión semántica de tres componentes: contiene dos puntos y por tanto no puede casar con el patrón de flotante — 1.10.0 es una cadena en todos los analizadores sin comillas, aunque ponérselas de todos modos no cuesta nada y ahorra tener que pensarlo.
- ¿Cuándo uso la barra vertical y cuándo el signo mayor que?
- Usa la barra vertical, el estilo literal, siempre que los saltos de línea formen parte del valor: un script de shell, un certificado, una clave SSH, una sentencia SQL, un fichero de configuración incrustado, un diagrama ASCII. Medido, un bloque literal de dos líneas devuelve "line one\nline two\n" — cada salto preservado, más uno al final. Usa el signo mayor que, el estilo plegado, para prosa que quieras cortar en el fichero fuente pero guardar en una sola línea: una descripción larga, un mensaje de ayuda, una plantilla de commit. El mismo bloque plegado devuelve "line one line two\n" — el salto interno se volvió un espacio. El plegado sí respeta las líneas en blanco como separaciones de párrafo: un bloque plegado con una línea vacía en medio devuelve "para one line a para one line b\npara two\n". Elige después el indicador de recorte de forma deliberada. La forma desnuda conserva exactamente un salto final, un signo menos lo elimina del todo, y un signo más los conserva todos. Un certificado PEM necesita su salto final: la forma desnuda es la correcta. Un token o un secreto de una línea no debe tenerlo: usa el menos. Este es el detalle que produce el misterioso desajuste de firma o el error de análisis de openssl sobre un valor que parece correcto en el fichero.
- ¿Son seguras las anclas y los alias en configuración de producción?
- En ficheros que escribes y revisas, sí — son la herramienta correcta para valores por defecto compartidos, y la clave de fusión produce exactamente el patrón de sobrescritura por entorno que la mayoría de despliegues necesita. Dos salvedades aplican incluso ahí. Un alias comparte el nodo en vez de copiarlo, verificado aquí por igualdad estricta entre dos entradas aliasadas, así que cualquier código que mute el árbol cargado en el sitio cambiará todas las apariciones a la vez. Y la clave de fusión es una función de YAML 1.1 arrastrada por convención más que una parte del esquema básico 1.2, de modo que el soporte varía por biblioteca — comprueba la tuya antes de depender de ella. En ficheros que llegan de fuera de tu organización, trata los alias como un vector de agotamiento de recursos. Un abanico de nueve sobre siete niveles fueron aquí 403 bytes de entrada y 39,46 MB de salida, un factor de 97 914, y nueve niveles alcanzarían 387 420 489 nodos lógicos. Conviene saber con precisión dónde cae el coste: js-yaml analizó todos esos casos en menos de dos milisegundos, porque los alias siguen siendo referencias compartidas; la factura de 363 milisegundos llegó al serializar el resultado. Así que la defensa es un tope de tamaño en la entrada, un límite de expansión de alias si tu biblioteca lo expone, y una regla contra copiar en profundidad o serializar un árbol cargado desde YAML no confiable.
- ¿El formateador YAML de este sitio cambia el significado de mis valores?
- No, porque nunca los analiza. Trabaja al nivel del texto: sustituye las tabulaciones por dos espacios, quita los espacios finales de cada línea, reduce las series de tres o más líneas en blanco a una, y en modo minificar elimina las líneas de comentario y las vacías. Ejecuta además una validación, el único error duro que la especificación define para los espacios, e informa del número de línea de cualquier tabulación encontrada en la indentación. Como ningún escalar se resuelve nunca, un NO desnudo sigue siendo los dos caracteres NO, 1.10 conserva su cero final, y tus escalares en bloque vuelven exactamente como los escribiste. Es una decisión de diseño deliberada: un formateador construido sobre un analizador haría pasar tu fichero por las reglas de resolución de ese analizador y te devolvería un documento con valores distintos, que es precisamente el fallo del que trata este artículo. Si quieres ver cómo se resuelve tu fichero, conviértelo a JSON — ese es el paso donde ocurre la resolución, y leer el JSON es la auditoría más rápida de tu entrecomillado.
Artículos que podrían interesarte
Todas las guías →Herramientas relacionadas
Fuentes
- 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
¿Has detectado un error en este artículo?