CVE-2023-28118 (published 2023-03-20T13:15Z)

kaml provides YAML support for kotlinx.serialization. Prior to version 0.53.0, applications that use kaml to parse untrusted input containing anchors and aliases may consume excessive memory and crash. Version 0.53.0 and later default to refusing to parse YAML documents containing anchors and aliases. There are no known workarounds.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-776
cwe_name: Improper Restriction of Recursive Entity References in DTDs ('XML Entity Expansion')
description: The product uses XML documents and allows their structure to be defined with a Document Type Definition (DTD), but it does not properly control the number of recursive definitions of entities.
extended_description: If the DTD contains a large number of nested or recursive entities, this can lead to explosive growth of data when parsed, causing a denial of service.
         

Fix information:
repo_url: https://github.com/charleskorn/kaml
commit_details: repo_url: https://github.com/charleskorn/kaml
author: Charles Korn
author_date: 2023-03-18 12:21:21+11:00
msg: Default to not parsing anchors and aliases to prevent "billion laughs"-style attacks.
repository: repo_url: https://github.com/charleskorn/kaml
repo_name: charleskorn/kaml
description: YAML support for kotlinx.serialization
repo_language: Kotlin
owner: charleskorn
file_changes: filename: YamlConfiguration.kt
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,7 @@ package com.charleskorn.kaml
  * * [sequenceStyle]: how sequences (aka lists and arrays) should be formatted. See [SequenceStyle] for an example of each
  * * [ambiguousQuoteStyle]: how strings should be escaped when [singleLineStringStyle] is [SingleLineStringStyle.PlainExceptAmbiguous] and the value is ambiguous
  * * [sequenceBlockIndent]: number of spaces to use as indentation for sequences, if [sequenceStyle] set to [SequenceStyle.Block]
+ * * [allowAnchorsAndAliases]: set to true to allow anchors and aliases when decoding YAML (defaults to `false`)
  */
 public data class YamlConfiguration constructor(
     internal val encodeDefaults: Boolean = true,
@@ -47,6 +48,7 @@ public data class YamlConfiguration constructor(
     internal val multiLineStringStyle: MultiLineStringStyle = singleLineStringStyle.multiLineStringStyle,
     internal val ambiguousQuoteStyle: AmbiguousQuoteStyle = AmbiguousQuoteStyle.DoubleQuoted,
     internal val sequenceBlockIndent: Int = 0,
+    internal val allowAnchorsAndAliases: Boolean = false,
 )
 
 public enum class PolymorphismStyle {

programming_language: Scala
filename: YamlException.kt
change_type: ModificationType.MODIFY
diff: @@ -98,3 +98,5 @@ public class NoAnchorForExtensionException(
     path: YamlPath,
 ) :
     YamlException("The key '$key' starts with the extension definition prefix '$extensionDefinitionPrefix' but does not define an anchor.", path)
+
+public class ForbiddenAnchorOrAliasException(message: String, path: YamlPath) : YamlException(message, path)

programming_language: Scala
filename: YamlReadingTest.kt
change_type: ModificationType.MODIFY
diff: @@ -1170,8 +1170,23 @@ class YamlReadingTest : DescribeSpec({
                     name: *name
                 """.trimIndent()
 
-                context("parsing that input") {
-                    val configuration = YamlConfiguration(extensionDefinitionPrefix = ".")
+                context("parsing anchors and aliases is disabled") {
+                    val configuration = YamlConfiguration(extensionDefinitionPrefix = ".", allowAnchorsAndAliases = false)
+                    val yaml = Yaml(configuration = configuration)
+
+                    it("throws an appropriate exception") {
+                        val exception = shouldThrow<ForbiddenAnchorOrAliasException> { yaml.decodeFromString(SimpleStructure.serializer(), input) }
+
+                        exception.asClue {
+                            it.message shouldBe "Parsing anchors and aliases is disabled."
+                            it.line shouldBe 1
+                            it.column shouldBe 18
+                        }
+                    }
+                }
+
+                context("parsing anchors and aliases is enabled") {
+                    val configuration = YamlConfiguration(extensionDefinitionPrefix = ".", allowAnchorsAndAliases = true)
                     val yaml = Yaml(configuration = configuration)
                     val result = yaml.decodeFromString(SimpleStructure.serializer(), input)
 

programming_language: Scala
filename: Yaml.kt
change_type: ModificationType.MODIFY
diff: @@ -60,7 +60,7 @@ public actual class Yaml(
 
     private fun parseToYamlNodeFromReader(source: Reader): YamlNode {
         val parser = YamlParser(source)
-        val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix)
+        val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix, configuration.allowAnchorsAndAliases)
         val node = reader.read()
         parser.ensureEndOfStreamReached()
         return node

programming_language: Scala
filename: YamlNodeReader.kt
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,7 @@ import java.util.Optional
 internal actual class YamlNodeReader(
     private val parser: YamlParser,
     private val extensionDefinitionPrefix: String? = null,
+    private val allowAnchorsAndAliases: Boolean = false,
 ) {
     private val aliases = mutableMapOf<Anchor, YamlNode>()
 
@@ -43,6 +44,10 @@ internal actual class YamlNodeReader(
 
         if (event is NodeEvent) {
             event.anchor.ifPresent {
+                if (!allowAnchorsAndAliases) {
+                    throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path)
+                }
+
                 aliases.put(it, node.withPath(YamlPath.forAliasDefinition(it.value, event.location)))
             }
 
@@ -186,6 +191,10 @@ internal actual class YamlNodeReader(
     }
 
     private fun readAlias(event: AliasEvent, path: YamlPath): YamlNode {
+        if (!allowAnchorsAndAliases) {
+            throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path)
+        }
+
         val anchor = event.anchor.get()
 
         val resolvedNode = aliases.getOrElse(anchor) {

programming_language: Scala
filename: YamlNodeReaderTest.kt
change_type: ModificationType.MODIFY
diff: @@ -312,7 +312,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser).read()
+                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
 
                 it("returns the expected list") {
                     result shouldBe
@@ -339,7 +339,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser).read()
+                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
 
                 it("returns the expected list, using the most-recently defined value each time the alias is referenced") {
                     result shouldBe
@@ -363,11 +363,11 @@ class YamlNodeReaderTest : DescribeSpec({
                 - *thing
             """.trimIndent()
 
-            describe("parsing that input") {
+            describe("parsing that input with anchor and alias parsing enabled") {
                 it("throws an appropriate exception") {
                     val exception = shouldThrow<UnknownAnchorException> {
                         val parser = YamlParser(input)
-                        YamlNodeReader(parser).read()
+                        YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                     }
 
                     exception.asClue {
@@ -378,6 +378,21 @@ class YamlNodeReaderTest : DescribeSpec({
                     }
                 }
             }
+
+            describe("parsing that input with anchor and alias parsing disabled") {
+                it("throws an appropriate exception") {
+                    val exception = shouldThrow<ForbiddenAnchorOrAliasException> {
+                        val parser = YamlParser(input)
+                        YamlNodeReader(parser, allowAnchorsAndAliases = false).read()
+                    }
+
+                    exception.asClue {
+                        it.message shouldBe "Parsing anchors and aliases is disabled."
+                        it.line shouldBe 2
+                        it.column shouldBe 3
+                    }
+                }
+            }
         }
 
         context("given some input representing a list of strings in flow style") {
@@ -652,7 +667,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser).read()
+                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                 val key1Path = YamlPath.root.withMapElementKey("key1", Location(1, 1))
                 val value1Path = key1Path.withMapElementValue(Location(1, 7))
                 val key2Path = YamlPath.root.withMapElementKey("key2", Location(2, 1))
@@ -1156,7 +1171,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser).read()
+                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
 
                 val firstItemPath = YamlPath.root.withListEntry(0, Location(1, 3))
                 val firstXPath = firstItemPath.withMapElementKey("x", Location(1, 13))
@@ -1206,7 +1221,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser).read()
+                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
 
                 val firstItemPath = YamlPath.root.withListEntry(0, Location(1, 3))
                 val firstXPath = firstItemPath.withMapElementKey("x", Location(1, 13))
@@ -1302,7 +1317,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser).read()
+                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
 
                 val firstItemPath = YamlPath.root.withListEntry(0, Location(1, 3))
                 val firstXPath = firstItemPath.withMapElementKey("x", Location(1, 13))
@@ -1365,7 +1380,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser).read()
+                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
 
                 val firstItemPath = YamlPath.root.withListEntry(0, Location(1, 3))
                 val firstXPath = firstItemPath.withMapElementKey("x", Location(1, 11))
@@ -1531,7 +1546,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input with an extension definition prefix defined") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".").read()
+                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".", allowAnchorsAndAliases = true).read()
 
                 val fooKeyPath = YamlPath.root.withMapElementKey("foo", Location(3, 1))
                 val fooValuePath = fooKeyPath.withMapElementValue(Location(4, 5))
@@ -1598,7 +1613,7 @@ class YamlNodeReaderTest : DescribeSpec({
 
             describe("parsing that input with an extension definition prefix defined") {
                 val parser = YamlParser(input)
-                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".").read()
+                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".", allowAnchorsAndAliases = true).read()
 
                 val keyPath = YamlPath.root
                     .withMerge(Location(4, 6))

programming_language: Scala