<rss version="2.0">
  <channel>
    <title>Yaml on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/yaml/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Fri, 21 Aug 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>Norway Is Not a Boolean</title>
      <link>https://llbbl.blog/2026/08/21/norway-is-not-a-boolean.html</link>
      <pubDate>Fri, 21 Aug 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/08/21/norway-is-not-a-boolean.html</guid>
      <description>&lt;p&gt;JSON&amp;rsquo;s problem is that its specification is too small. It tells you &lt;code&gt;9007199254740993&lt;/code&gt; is a well-formed number and then declines to say which number.&lt;/p&gt;
&lt;p&gt;YAML went the other way. The 1.2.2 specification is a book. It has a formal grammar, a chapter on recommended schemas, and an answer for nearly everything. And it will still read your config file and decide, without asking, that Norway is false.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-norway-problem&#34;&gt;The Norway Problem&lt;/h2&gt;
&lt;p&gt;Here is a config file. Every value in it is a string that a human would read as a string.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;country&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;NO&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;duration&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;:&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;mode&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0755&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three parsers, on those exact bytes:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;PyYAML 6.0.3   {&amp;#39;country&amp;#39;: False, &amp;#39;duration&amp;#39;: 80,     &amp;#39;mode&amp;#39;: 493}
ruamel.yaml    {&amp;#39;country&amp;#39;: &amp;#39;NO&amp;#39;,  &amp;#39;duration&amp;#39;: &amp;#39;1:20&amp;#39;, &amp;#39;mode&amp;#39;: 755}
js-yaml        {&amp;#34;country&amp;#34;:&amp;#34;NO&amp;#34;,   &amp;#34;duration&amp;#34;:&amp;#34;1:20&amp;#34;,  &amp;#34;mode&amp;#34;:755}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;NO&lt;/code&gt; is the ISO 3166 code for Norway. PyYAML returns the boolean &lt;code&gt;false&lt;/code&gt;, because YAML 1.1 recognized twenty-two spellings of true and false, and &lt;code&gt;NO&lt;/code&gt; is one of them. The spec lists them as a single regular expression:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;y|Y|yes|Yes|YES|n|N|no|No|NO
|true|True|TRUE|false|False|FALSE
|on|On|ON|off|Off|OFF
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Count them. Twenty-two. Six of those are country codes, single letters, or ordinary English words that appear in real data. Nothing in the file said &amp;ldquo;this is a boolean.&amp;rdquo; The parser inferred it from the shape of the text, and the shape of the text was two letters.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1:20&lt;/code&gt; became &lt;code&gt;80&lt;/code&gt; because YAML 1.1 supported sexagesimal integers, so a duration is read as base 60. One times sixty, plus twenty.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0755&lt;/code&gt; became &lt;code&gt;493&lt;/code&gt; because a leading zero meant octal. That is a file mode that no longer means what it says.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;it-was-fixed-in-2009&#34;&gt;It Was Fixed in 2009&lt;/h2&gt;
&lt;p&gt;This is the part that makes YAML different from the other formats in this series.&lt;/p&gt;
&lt;p&gt;CSV never had a standard. Markdown had too many. YAML had exactly one problem, everybody agreed it was a problem, and the working group fixed it. YAML 1.2 arrived in 2009 and threw all of it out. Base 60 is gone. Implicit octal is gone. The Core schema recognizes &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; and their case variants, and nothing else.&lt;/p&gt;
&lt;p&gt;Seventeen years later, the two YAML 1.2 parsers above return strings, and PyYAML returns &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;PyYAML implements YAML 1.1. It is the default YAML library for Python, it is what &lt;code&gt;pip install pyyaml&lt;/code&gt; gives you, and the specification it implements was superseded when the iPhone 3GS was current. The fix exists. It shipped. Most of the ecosystem simply stayed where it was, because changing the type of &lt;code&gt;NO&lt;/code&gt; in a minor release breaks every config file that relied on it.&lt;/p&gt;
&lt;p&gt;A format can be fixed and still be broken, if the fix arrives after the implementations do.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;everything-else-that-isnt-a-string&#34;&gt;Everything Else That Isn&amp;rsquo;t a String&lt;/h2&gt;
&lt;p&gt;The country-code case is famous. It is not the only one, and the rest are quieter:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;version: 1.10   -&amp;gt;  1.1        (float, and .10 became .1)
build: 010      -&amp;gt;  8          (octal)
port: 8080      -&amp;gt;  8080       (int, fine, until you concatenate it)
answers: [y, n] -&amp;gt;  [&amp;#39;y&amp;#39;, &amp;#39;n&amp;#39;] (strings)
answers: [yes, no] -&amp;gt; [True, False]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first one is the one that should bother you. A semantic version of &lt;code&gt;1.10&lt;/code&gt; parses as the float &lt;code&gt;1.1&lt;/code&gt;, which is a different version, and it does it silently in a file whose entire job is to record which version you meant.&lt;/p&gt;
&lt;p&gt;And note the last two lines. &lt;code&gt;y&lt;/code&gt; and &lt;code&gt;n&lt;/code&gt; stay strings in PyYAML while &lt;code&gt;yes&lt;/code&gt; and &lt;code&gt;no&lt;/code&gt; become booleans, because PyYAML&amp;rsquo;s resolver implements a narrower set than the 1.1 spec&amp;rsquo;s regexp advertises. So the answer to &amp;ldquo;does this parser coerce single letters&amp;rdquo; is neither yes nor no. It is &amp;ldquo;some of them, and you have to test.&amp;rdquo;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;two-ways-to-weaponize-the-convenience&#34;&gt;Two Ways to Weaponize the Convenience&lt;/h2&gt;
&lt;p&gt;YAML has anchors. You define a node once with &lt;code&gt;&amp;amp;name&lt;/code&gt; and reference it with &lt;code&gt;*name&lt;/code&gt;. It is a useful feature for config files with repeated blocks, and it composes.&lt;/p&gt;
&lt;p&gt;That is the problem. It composes exponentially.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;a&lt;/span&gt;: &lt;span style=&#34;color:#75715e&#34;&gt;&amp;amp;a&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;lol&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;b&lt;/span&gt;: &lt;span style=&#34;color:#75715e&#34;&gt;&amp;amp;b&lt;/span&gt; [&lt;span style=&#34;color:#75715e&#34;&gt;*a,*a,*a,*a,*a,*a,*a,*a,*a]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;c&lt;/span&gt;: &lt;span style=&#34;color:#75715e&#34;&gt;&amp;amp;c&lt;/span&gt; [&lt;span style=&#34;color:#75715e&#34;&gt;*b,*b,*b,*b,*b,*b,*b,*b,*b]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;d&lt;/span&gt;: &lt;span style=&#34;color:#75715e&#34;&gt;&amp;amp;d&lt;/span&gt; [&lt;span style=&#34;color:#75715e&#34;&gt;*c,*c,*c,*c,*c,*c,*c,*c,*c]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;e&lt;/span&gt;: &lt;span style=&#34;color:#75715e&#34;&gt;&amp;amp;e&lt;/span&gt; [&lt;span style=&#34;color:#75715e&#34;&gt;*d,*d,*d,*d,*d,*d,*d,*d,*d]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That file is 202 bytes. Expanding it produces 74,732 nodes, of which 59,049 are copies of the string &lt;code&gt;lol&lt;/code&gt;. Add one more line and multiply by nine. This is the billion laughs attack, and the important detail is that &lt;code&gt;safe_load&lt;/code&gt; does not stop it. Aliases are not a dangerous tag, they are a core language feature working as designed.&lt;/p&gt;
&lt;p&gt;The second way is tags. YAML can annotate a node with a type, and PyYAML historically honored tags that construct arbitrary Python objects:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;!!python/object/apply:os.system&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;args&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;id&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;yaml.load()&lt;/code&gt; on untrusted input would run that. It became &lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2017-18342&#34;&gt;CVE-2017-18342&lt;/a&gt;, CVSS 9.8, published June 2018, with a description that is unusually blunt for the genre: &lt;em&gt;&amp;ldquo;In PyYAML before 5.1, the yaml.load() API could execute arbitrary code if used with untrusted data.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The fix took two releases and three years. PyYAML 5.1 deprecated the unsafe default in March 2019. PyYAML 6.0 finally made the &lt;code&gt;Loader&lt;/code&gt; argument mandatory in October 2021, so the dangerous call stopped being the short one:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; yaml.load(&amp;#39;a: 1&amp;#39;)
TypeError: load() missing 1 required positional argument: &amp;#39;Loader&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The vulnerability was published in 2018. Making the unsafe call harder to type than the safe one landed in 2021.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-to-do-about-it&#34;&gt;What To Do About It&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quote anything that isn&amp;rsquo;t obviously a number.&lt;/strong&gt; Country codes, versions, file modes, git SHAs, anything a human would call an identifier. Quoting is never wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Know which YAML version your parser speaks.&lt;/strong&gt; If it is Python, assume 1.1 and the Norway problem unless you chose otherwise. &lt;code&gt;ruamel.yaml&lt;/code&gt; gives you 1.2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never call &lt;code&gt;yaml.load&lt;/code&gt; on input you did not write.&lt;/strong&gt; &lt;code&gt;safe_load&lt;/code&gt;, always. On PyYAML 6 the language makes you say which you meant, which is the correct design.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bound the input.&lt;/strong&gt; &lt;code&gt;safe_load&lt;/code&gt; is not a defense against alias expansion. If you parse YAML you did not author, cap the document size before it reaches the parser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a schema.&lt;/strong&gt; The value of a schema here is not validation, it is that it declares the type instead of letting the parser guess it from the characters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;YAML&amp;rsquo;s failure is the opposite of JSON&amp;rsquo;s, and it produces the same result. JSON declined to say what values mean, so implementations disagreed. YAML said what values mean in enormous detail, got it wrong in 2005, corrected it in 2009, and the correction never fully landed.&lt;/p&gt;
&lt;p&gt;Next in this series is XML, which is the one format here that did specify everything. It has a schema language, a query language, a transformation language, and a namespace system. It is worth asking what all of that bought.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://yaml.org/spec/1.2.2/&#34;&gt;YAML 1.2.2 Specification&lt;/a&gt; — October 2021; the schemas chapter and the rule that tabs &amp;ldquo;must not be used in indentation, since different systems treat tabs differently&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://yaml.org/type/bool.html&#34;&gt;YAML 1.1 Boolean type&lt;/a&gt; — the twenty-two-form regexp, working draft dated 2005&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2017-18342&#34;&gt;CVE-2017-18342&lt;/a&gt; — the &lt;code&gt;yaml.load()&lt;/code&gt; RCE, CVSS 9.8&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/yaml/pyyaml/blob/main/CHANGES&#34;&gt;PyYAML CHANGES&lt;/a&gt; — 5.1 (2019) deprecated the unsafe default, 6.0 (2021) made &lt;code&gt;Loader&lt;/code&gt; required&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>