Ir para o conteúdo
Allin

O YAML parece simpático e morde

Publicado a 12/08/2025 · 18 min de leitura · Ferramentas para programadores

Daniel Okonkwo

Daniel OkonkwoProgramador front-end e redator de Tecnologia na Allin

Desempenho web · Formatos de ficheiro

Verificado a partir de 6 fontes

Ver perfil
Em resumo

O YAML 1.2 declara o JSON um subconjunto, pelo que todo o documento JSON é YAML válido. O que o YAML acrescenta por cima é um passo de resolução que adivinha um tipo para cada escalar sem aspas, e essa adivinha mudou entre versões da especificação. Sob o YAML 1.1 os tokens y, yes, no, on e off resolvem para booleanos: é o famoso problema norueguês, em que o código de país NO se torna false. O YAML 1.2 retirou-os do esquema básico, pelo que um analisador 1.2 os deixa como strings. Qual o comportamento que obtém depende inteiramente do seu analisador, não do seu ficheiro. Medido sobre um mesmo documento: o js-yaml 4.3.0, que se descreve como analisador YAML 1.2, devolve a string "no"; o PyYAML 6.0.3, um analisador YAML 1.1, devolve False. A mesma fratura atinge o 01234, que vale 1234 em 1.2 e o octal 668 em 1.1, e o 12:30:00, string em 1.2 e inteiro 45000 em 1.1. Alguns perigos sobreviveram intactos à mudança de versão: 1.10 é o flutuante 1,1 em ambos, portanto um número de versão perde um dígito em silêncio. Junte indentação significativa onde as tabulações estão terminantemente proibidas, dois estilos de escalar em bloco com três modos de corte, e âncoras que levam 403 bytes a 39 MB: a regra prática escreve-se sozinha — cite toda a string que possa ser lida como outra coisa.

O YAML é JSON mais uma camada de inferência de tipos, e a inferência é a parte perigosa. O mesmo ficheiro passado por um analisador YAML 1.2 e por um 1.1: no é uma string num e false no outro, 01234 é 1234 num e 668 no outro, e 12:30:00 é um número num dos dois.

Um superconjunto de JSON, mais uma ideia perigosa

O YAML 1.2 enuncia a relação explicitamente: o JSON é um subconjunto do YAML, e um processador YAML conforme aceita qualquer documento JSON. Execute-o e a afirmação aguenta — dar {"a": 1, "b": [1,2,3]} ao js-yaml devolve exatamente o objeto esperado. Portanto tudo o que o artigo anterior dizia sobre os tipos em falta no JSON aplica-se aqui também, sem alterações. O YAML não lhe dá um tipo inteiro distinto de um flutuante, nem um tipo binário, nem um esquema.

O que o YAML acrescenta é conforto para humanos: comentários, sem aspas nas chaves, sem aspas na maioria das strings, sem chavetas, sem vírgulas, texto em bloco que conserva as suas quebras de linha, e um mecanismo de referência para escrever um valor uma vez e reutilizá-lo. São melhorias reais para um ficheiro mantido por uma pessoa, e é por isso que o YAML governa a configuração da maior parte das ferramentas de implantação em uso hoje.

A ideia perigosa é a que torna tudo isso possível. Se as chaves e as strings não precisam de aspas, o analisador tem de decidir o que significa um token sem aspas, e o YAML chama a esse passo resolução: um escalar simples é confrontado com um conjunto de expressões regulares e recebe um tipo. É daí que vem cada surpresa deste artigo, e é também a parte da linguagem que mudou entre o YAML 1.1 e o YAML 1.2 — logo o mesmo ficheiro pode significar duas coisas diferentes consoante a biblioteca que o lê.

O problema norueguês, e que versão da especificação tem realmente

O YAML 1.1 definia um tipo booleano de repertório generoso: true e false, mas também yes e no, on e off, e no repositório de tipos as letras soltas y e n. Um ficheiro que lista códigos de país transforma portanto a Noruega num booleano, porque o código ISO da Noruega é NO. É todo o famoso erro, e não é um defeito do analisador: é a especificação a funcionar como está escrita.

O YAML 1.2 corrigiu-o encolhendo o esquema básico. A etiqueta booleana só casa já com true e false, nalgumas capitalizações. Todo o resto continua a ser uma string. Mas o seu ficheiro não traz versão, e quase ninguém escreve a diretiva %YAML que declararia uma, portanto a versão que se aplica é a que a sua biblioteca implementa — e ambas continuam largamente em produção hoje.

Verifique, pois, não suponha. O caminho YAML deste sítio usa o js-yaml 4.3.0, cuja descrição de pacote diz «YAML 1.2 parser and serializer», e passar o documento de teste por ele devolve a string "no" para o caso norueguês — sob os seus quatro esquemas, do failsafe ao predefinido. O mesmo documento pelo PyYAML 6.0.3, um analisador YAML 1.1, devolve o False do Python. Os mesmos bytes, sentidos opostos, e nenhuma das bibliotecas está errada. Tire três minutos e passe o seu próprio ficheiro pelo seu próprio analisador antes de acreditar em fosse o que fosse escrito sobre isto na Internet, incluindo isto.

Números que não são os que digitou

O mais caro sobreviveu intacto à mudança de versão. Escreva version: 1.10 e ambos os analisadores devolvem o número de vírgula flutuante 1,1 — o js-yaml dá 1.1, o PyYAML dá 1.1 como flutuante. O YAML resolveu-o como número, e um número não tem zero final: a sua versão 1.10 é agora a versão 1.1 e ordena antes de 1.2 e de 1.9. Cite-a e sobrevive: "1.10" volta como a string 1.10 em ambos. Os números de versão, de peça, de modelo e tudo o que tenha um último dígito com significado têm de ser citados, em toda a versão da especificação.

Os zeros iniciais são piores, porque as duas versões divergem no como. Escreva zip: 01234 e o js-yaml devolve o número 1234 — o zero inicial simplesmente desapareceu, porque o padrão inteiro do YAML 1.2 é um decimal vulgar. O PyYAML devolve 668, porque sob o YAML 1.1 um zero inicial significa octal, e 1234 lido em base 8 vale 668. Assim, um código postal, um código de balcão ou um número de conta escrito sem aspas fica corrompido por ambos os analisadores, em dois valores errados diferentes. No sentido inverso, a notação octal própria do YAML 1.2, 0o17, resolve para 15 no js-yaml e continua a ser a string "0o17" no PyYAML, que não reconhece a forma recente.

A última armadilha numérica é a sexagesimal, e morreu com o YAML 1.1. Essa versão resolvia os grupos de dígitos separados por dois pontos como inteiros de base 60: 12:30:00 tornava-se 45000 e 22:22 tornava-se 1342. Execute: o PyYAML devolve exatamente esses dois inteiros, e o js-yaml devolve as strings. Um horário à crontab, uma duração, um fragmento de endereço MAC ou uma marca temporal musical escrita sem aspas num ficheiro 1.1 torna-se um inteiro sem relação evidente com o que escreveu — 45000 é o número de segundos em doze horas e meia, o que é pelo menos lógico, e 1342 é 22 vezes 60 mais 22, que não é o que ninguém quis dizer.

Espaços, tabulações proibidas e os dois escalares em bloco

A indentação é estrutura no YAML, o que significa que os espaços não são cosméticos e um formatador não os pode redistribuir livremente. A especificação proíbe por completo as tabulações na indentação — não desaconselha, proíbe — porque uma tabulação não tem largura definida e o analisador não teria como saber a que profundidade queria estar. Dê uma lista indentada com tabulações ao js-yaml e ele para com uma queixa precisa: tab characters must not be used in indentation, linha 2 coluna 1. Esse erro é a falha YAML mais comum num editor que converte solicitamente os espaços iniciais.

O texto multilinha usa um de dois estilos em bloco, e a diferença é exatamente o que acontece às suas quebras de linha. O estilo literal, escrito com uma barra vertical, conserva cada quebra: um bloco de duas linhas volta como "line one\nline two\n". O estilo dobrado, escrito com um sinal de maior, junta as linhas consecutivas com um espaço: o mesmo bloco volta como "line one line two\n". O dobrado conserva contudo uma linha em branco como quebra real, pelo que um bloco dobrado de dois parágrafos devolve "para one line a para one line b\npara two\n" — um parágrafo junto e depois uma quebra genuína.

Por cima do estilo há um indicador de corte que decide a quebra de linha final, e é a parte de que as pessoas se esquecem. O valor por omissão, escrito sem mais nada, é clip: conserva-se exatamente uma quebra final. Um sinal de menos elimina-a, pelo que o mesmo bloco volta como "line one\nline two" sem quebra final nenhuma. Um sinal de mais conserva todas as linhas em branco finais: um bloco seguido de uma linha vazia devolve "line one\nline two\n\n". Isto importa mais do que parece: um certificado, uma chave SSH ou um script de shell embutido num ficheiro de configuração costuma precisar da sua quebra final, ao passo que um token ou uma palavra-passe embutida normalmente não deve tê-la. Errar produz uma discrepância sobre um valor que parece idêntico em todos os diffs.

Âncoras e aliases: uma função real que é também uma bomba

Uma âncora nomeia um nó com um e comercial, um alias remete para ele com um asterisco, e a chave de fusão traz as chaves de um mapeamento para outro. Juntas eliminam a maior fonte de deriva na configuração: escreva os seus valores por omissão uma vez e depois sobreponha os dois que diferem por ambiente. Execute e faz exatamente o que quer — um bloco base com um tempo de espera de 30 e três tentativas, fundido em dev com o tempo levado a 5, dá ao dev um tempo de 5 e três tentativas, enquanto o prod mantém 30 e 3.

Há uma subtileza que vale a pena conhecer antes de confiar nisso: um alias não copia, partilha. Carregue um documento em que duas entradas de lista façam alias para a mesma âncora e as duas entradas são o mesmo objeto — a igualdade estrita entre elas é verdadeira, e também com o original. Mute uma depois do carregamento e mutou-as todas. Isto é inócuo para configuração só de leitura e uma armadilha real em código que normaliza ou remenda a árvore carregada no lugar.

Essa mesma partilha é o que torna possível o ataque por expansão. Encadeie âncoras de modo que cada nível seja uma lista de nove referências ao nível inferior: o tamanho do documento lógico é nove elevado à profundidade enquanto o ficheiro permanece minúsculo. Medido com o js-yaml: quatro níveis são 241 bytes de YAML e 54 127 bytes de JSON, um fator de 225; seis níveis são 349 bytes e 4,38 MB, um fator de 12 563; sete níveis são 403 bytes e 39,46 MB, um fator de 97 914. A nove níveis a contagem de nós lógicos chega a 387 420 489. Repare onde cai o custo real: o js-yaml analisou tudo isto em menos de dois milissegundos, porque os aliases são referências partilhadas e o grafo em memória permanece pequeno. Foi serializar o resultado que demorou 363 milissegundos a sete níveis. Portanto a defesa não é só um limite do analisador: é recusar percorrer, copiar em profundidade ou serializar uma árvore vinda de YAML não fiável, mais um teto de tamanho na entrada e um limite de expansão de aliases se a sua biblioteca o oferecer.

A regra, e o que um formatador pode e não pode fazer por si

Cite toda a string que possa ser lida como outra coisa. Na prática é uma lista curta e memorizável: tudo o que seja ou contenha yes, no, on, off, y, n, true ou false; todo o código de país, em especial NO; todo o valor com zero inicial; todo o número de versão ou de peça com zero final depois de uma vírgula decimal; tudo o que leve dois pontos, como uma hora ou uma duração; as palavras null e none e o til; e tudo o que pareça um número mas seja na verdade um identificador. As plicas são a forma mais segura, porque dentro delas nada é um escape: um caminho do Windows ou uma expressão regular passa intacto.

Uma coisa vale a pena esclarecer, porque é um equívoco comum. O formatador YAML deste sítio não analisa YAML. Normaliza o texto — converte as tabulações em dois espaços, retira os espaços finais, reduz as séries de linhas em branco e, para minificar, elimina comentários e linhas vazias — e verifica à parte o único erro duro, uma tabulação na indentação. Nunca resolve um escalar, pelo que não pode transformar o seu NO em false nem o seu 1.10 em 1,1, e não reformatará os seus escalares em bloco. É deliberado: um formatador que fizesse o seu ficheiro dar uma volta por um analisador aplicaria em silêncio a versão das regras de resolução desse analisador e devolver-lhe-ia um documento diferente.

Pela mesma razão, trate qualquer conversão de YAML para JSON como um passo com perda e inspecione o resultado. Converter é exatamente o momento em que as regras de resolução disparam, portanto é também a forma mais barata de descobrir o que o seu analisador acha realmente que o seu ficheiro diz — dê-lhe a sua configuração, leia o JSON, e cada erro de citação deste artigo torna-se visível numa passagem.

O mesmo documento por dois analisadores: js-yaml 4.3.0 (analisador YAML 1.2) e PyYAML 6.0.3 (analisador YAML 1.1)
Escrito no ficheirojs-yaml 4.3.0 (YAML 1.2)PyYAML 6.0.3 (YAML 1.1)Forma segura
no"no" (string)False (booleano)'no'
NO (o código ISO da Noruega)"NO" (string)False (booleano)'NO'
yes"yes" (string)True (booleano)'yes' ou true
1.10 (um número de versão)1,1 (número — o zero desapareceu)1,1 (flutuante — o zero desapareceu)"1.10"
01234 (um código postal)1234 (número — decimal)668 (inteiro — lido em octal)"01234"
12:30:00 (uma hora do dia)"12:30:00" (string)45000 (inteiro — base 60)"12:30:00"
0o17 (notação octal do YAML 1.2)15 (número)"0o17" (string — forma desconhecida em 1.1)Escreva antes o valor decimal
Formatador / validador YAMLArrume YAML — normalize a indentação, converta tabulações em espaços e sinalize erros de tabulação.Experimentar a ferramenta

Perguntas frequentes

O problema norueguês está corrigido, e como sei que versão o meu analisador implementa?
Está corrigido na especificação e não necessariamente no seu programa. O YAML 1.2 retirou yes, no, on e off da etiqueta booleana do esquema básico, pelo que um analisador 1.2 os deixa como strings. O YAML 1.1 resolvia-os a todos, mais as letras soltas y e n do seu repositório de tipos, daí o código de país ISO NO se ter tornado false. O seu ficheiro não declara versão — a diretiva %YAML existe mas praticamente ninguém a escreve — portanto o comportamento vem inteiramente da biblioteca. O teste fiável leva um minuto: carregue um documento de duas linhas com uma chave cujo valor simples seja no, e imprima o tipo do resultado. Medido aqui, o js-yaml 4.3.0 devolve a string "no", e fá-lo sob os quatro esquemas que traz, do failsafe ao predefinido. O PyYAML 6.0.3 devolve o False do Python. Ambas são implementações corretas de versões diferentes da especificação. Note também que as implementações divergem quanto às formas de uma letra mesmo dentro do 1.1 — o PyYAML deixa um y solto e um n solto como strings — portanto testar vale mais do que ler. E seja qual for a resposta, citar o valor é grátis e funciona em todas as versões.
Porque é que o meu número de versão 1.10 se tornou 1,1?
Porque casou com o padrão de flutuante, e um flutuante não tem memória dos zeros finais. Ambos os analisadores concordam aqui — o js-yaml devolve 1.1 e o PyYAML devolve 1.1 como flutuante — portanto isto não é uma questão de versão da especificação e citar é a única correção. O estrago vai além de uma falha de apresentação. A ordenação parte-se, porque como número 1,1 fica entre 1,09 e 1,2 ao passo que como string de versão 1.10 vem depois de 1.9. A igualdade parte-se, porque uma procura da versão chamada 1.10 já não encontra a chave. E reserializar o ficheiro escreve 1.1 de volta no disco: o erro torna-se permanente no seu repositório e o diff mostra uma alteração de um carácter com ar plausível. A mesma armadilha apanha qualquer identificador com ponto de dois componentes: um número de capítulo, uma revisão de firmware, uma versão de esquema, um código de produto decimal. Escreva-o como "1.10" entre aspas. Se precisar de semântica de ordem a sério, use uma versão semântica de três componentes: contém dois pontos e portanto não pode casar com o padrão de flutuante — 1.10.0 é uma string em todos os analisadores sem aspas, embora pô-las na mesma não custe nada e evite ter de pensar nisso.
Quando uso a barra vertical e quando o sinal de maior?
Use a barra vertical, o estilo literal, sempre que as quebras de linha façam parte do valor: um script de shell, um certificado, uma chave SSH, uma instrução SQL, um ficheiro de configuração embutido, um diagrama ASCII. Medido, um bloco literal de duas linhas devolve "line one\nline two\n" — cada quebra preservada, mais uma no fim. Use o sinal de maior, o estilo dobrado, para prosa que queira cortar no ficheiro de origem mas guardar numa só linha: uma descrição longa, uma mensagem de ajuda, um modelo de commit. O mesmo bloco dobrado devolve "line one line two\n" — a quebra interna tornou-se um espaço. O dobrado respeita ainda as linhas em branco como separações de parágrafo: um bloco dobrado com uma linha vazia no meio devolve "para one line a para one line b\npara two\n". Escolha depois o indicador de corte deliberadamente. A forma nua conserva exatamente uma quebra final, um sinal de menos elimina-a por completo, e um sinal de mais conserva-as todas. Um certificado PEM precisa da sua quebra final: a forma nua é a certa. Um token ou um segredo de uma linha não deve tê-la: use o menos. É este o pormenor que produz a misteriosa discrepância de assinatura ou o erro de análise do openssl sobre um valor que parece correto no ficheiro.
As âncoras e os aliases são seguros em configuração de produção?
Em ficheiros que escreve e revê, sim — são a ferramenta certa para valores por omissão partilhados, e a chave de fusão produz exatamente o padrão de sobreposição por ambiente de que a maioria das implantações precisa. Duas ressalvas aplicam-se mesmo aí. Um alias partilha o nó em vez de o copiar, verificado aqui por igualdade estrita entre duas entradas com alias, pelo que qualquer código que mute a árvore carregada no lugar mudará todas as ocorrências de uma vez. E a chave de fusão é uma função do YAML 1.1 arrastada por convenção e não parte do esquema básico 1.2, pelo que o suporte varia por biblioteca — verifique a sua antes de depender dela. Em ficheiros que chegam de fora da sua organização, trate os aliases como um vetor de esgotamento de recursos. Um leque de nove sobre sete níveis foram aqui 403 bytes de entrada e 39,46 MB de saída, um fator de 97 914, e nove níveis chegariam a 387 420 489 nós lógicos. Vale a pena saber com precisão onde cai o custo: o js-yaml analisou todos esses casos em menos de dois milissegundos, porque os aliases continuam a ser referências partilhadas; a fatura de 363 milissegundos chegou ao serializar o resultado. Portanto a defesa é um teto de dimensão na entrada, um limite de expansão de aliases se a sua biblioteca o expuser, e uma regra contra copiar em profundidade ou serializar uma árvore carregada de YAML não fiável.
O formatador YAML deste sítio muda o significado dos meus valores?
Não, porque nunca os analisa. Trabalha ao nível do texto: substitui as tabulações por dois espaços, retira os espaços finais de cada linha, reduz as séries de três ou mais linhas em branco a uma, e em modo de minificação elimina as linhas de comentário e as vazias. Corre ainda uma validação, o único erro duro que a especificação define para os espaços, e indica o número de linha de qualquer tabulação encontrada na indentação. Como nenhum escalar é alguma vez resolvido, um NO nu continua a ser os dois caracteres NO, 1.10 mantém o seu zero final, e os seus escalares em bloco voltam exatamente como os escreveu. É uma escolha de conceção deliberada: um formatador construído sobre um analisador faria o seu ficheiro passar pelas regras de resolução desse analisador e devolver-lhe-ia um documento com valores diferentes, que é precisamente a falha de que trata este artigo. Se quiser ver como o seu ficheiro se resolve, converta-o antes para JSON — é esse o passo em que a resolução acontece, e ler o JSON é a auditoria mais rápida da sua citação.

Artigos que podem interessar-lhe

Todos os guias
ExplicaçãoDe XML para JSON: atributos, repetição e a armadilha do array de um só elementoDois documentos que só diferem no número de filhos produzem duas formas JSON diferentes, e nenhum conversor os distingue sem um esquema. Além do que este faz mesmo com os atributos, o conteúdo misto e os espaços — e a única coisa que continua a não conseguir registar.ComparaçãoJSON vs XML: qual a diferença?JSON e XML armazenam ambos dados estruturados como texto, mas com compromissos diferentes. Eis como cada um se parece, onde cada um vence e como escolher.ExplicaçãoJSON é mais simples do que julga, e é esse o problemaO JSON não tem tipo inteiro, nem tipo data, nem comentários, nem esquema. Cada uma dessas ausências produz um erro concreto: um identificador de 19 dígitos volta desviado em 21, um carimbo temporal torna-se uma string que ninguém acordou, NaN não se escreve e as chaves duplicadas são legais. Tudo executado, em duas linguagens.GuiaEmbelezar ou minificar: para que serve cada um e o que faz ao pesoQuatro folhas de estilo reais passadas pelo minificador, medidas em bruto e depois de gzip. Retirar todos os espaços poupou 48, 103, 104 e 147 bytes comprimidos; retirar os comentários, 57, 1 358, 2 420 e 1 042. E as cinco entradas que este minificador parte.TutorialConstruir uma tabela Markdown de raiz, sem contar travessões à mãoA coisa mais pequena que ainda é uma tabela são duas linhas: uma linha de cabeçalho e uma linha delimitadora. Aqui está porque é que a segunda é obrigatória em GitHub Flavored Markdown, onde as tabelas de barras não existem de todo, e o que um gerador faz que escrever à mão não consegue.GuiaColar uma tabela num pull request: o que parte, e os dois carateres que partem tudoUma tabela Markdown proíbe exatamente dois carateres dentro de uma célula: a barra vertical e a quebra de linha. Aqui está o que cada um faz, como um conversor os trata, porque é que o escape tem de ser aplicado na ordem certa, e porque é que o alinhamento da fonte nunca conta.

Ferramentas relacionadas

Fontes

Detetaste um erro neste artigo?