CVE-2023-28118: Improper Restriction of Recursive Entity References in DTDs ('XML Entity Expansion') (CWE-776)
value: 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.
CWE 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.
CVSS v3 base score: 7.5
Published: 2023-03-20T13:15Z
Repository: https://github.com/charleskorn/kaml
Language: Kotlin
Files changed: src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt, src/commonMain/kotlin/com/charleskorn/kaml/YamlException.kt, src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt, src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt, src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt, src/jvmTest/kotlin/com/charleskorn/kaml/YamlNodeReaderTest.kt
Commit date: 2023-03-18 12:21:21 +1100
Commit message: Default to not parsing anchors and aliases to prevent "billion laughs"-style attacks.

Diff: diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt
index 6815f2e..e3f7607 100644
--- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt
+++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlConfiguration.kt
@@ -31,10 +31,11 @@ package com.charleskorn.kaml
  * * [encodingIndentationSize]: number of spaces to use as indentation when encoding objects as YAML
  * * [breakScalarsAt]: maximum length of scalars when encoding objects as YAML (scalars exceeding this length will be split into multiple lines)
  * * [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,
     internal val strictMode: Boolean = true,
     internal val extensionDefinitionPrefix: String? = null,
@@ -45,10 +46,11 @@ public data class YamlConfiguration constructor(
     internal val sequenceStyle: SequenceStyle = SequenceStyle.Block,
     internal val singleLineStringStyle: SingleLineStringStyle = SingleLineStringStyle.DoubleQuoted,
     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 {
     Tag,
     Property,
diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlException.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlException.kt
index 1a758ee..0546c20 100644
--- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlException.kt
+++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlException.kt
@@ -96,5 +96,7 @@ public class NoAnchorForExtensionException(
     public val key: String,
     public val extensionDefinitionPrefix: String,
     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)
diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
index 4fefab4..14fd188 100644
--- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
+++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
@@ -1168,12 +1168,27 @@ class YamlReadingTest : DescribeSpec({
                     .some-extension: &name Jamie
 
                     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)
 
                     it("deserializes it to a Kotlin object, replacing the reference to the extension with the extension") {
                         result shouldBe SimpleStructure("Jamie")
diff --git a/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt b/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt
index 16773f4..3ad3817 100644
--- a/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt
+++ b/src/jvmMain/kotlin/com/charleskorn/kaml/Yaml.kt
@@ -58,11 +58,11 @@ public actual class Yaml(
 
     public fun parseToYamlNode(source: InputStream): YamlNode = parseToYamlNodeFromReader(InputStreamReader(source))
 
     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
     }
 
diff --git a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt
index 3ed6652..5f5c449 100644
--- a/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt
+++ b/src/jvmMain/kotlin/com/charleskorn/kaml/YamlNodeReader.kt
@@ -28,10 +28,11 @@ import org.snakeyaml.engine.v2.events.SequenceStartEvent
 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>()
 
     actual fun read(): YamlNode = readNode(YamlPath.root)
 
@@ -41,10 +42,14 @@ internal actual class YamlNodeReader(
         val event = parser.consumeEvent(path)
         val node = readFromEvent(event, path)
 
         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)))
             }
 
             return node to event.anchor.orElse(null)
         }
@@ -184,10 +189,14 @@ internal actual class YamlNodeReader(
 
         return merged
     }
 
     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) {
             throw UnknownAnchorException(anchor.value, path.withError(event.location))
         }
diff --git a/src/jvmTest/kotlin/com/charleskorn/kaml/YamlNodeReaderTest.kt b/src/jvmTest/kotlin/com/charleskorn/kaml/YamlNodeReaderTest.kt
index c93f1f4..f988bad 100644
--- a/src/jvmTest/kotlin/com/charleskorn/kaml/YamlNodeReaderTest.kt
+++ b/src/jvmTest/kotlin/com/charleskorn/kaml/YamlNodeReaderTest.kt
@@ -310,11 +310,11 @@ class YamlNodeReaderTest : DescribeSpec({
                 - *thing
             """.trimIndent()
 
             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
                         YamlList(
                             listOf(
@@ -337,11 +337,11 @@ class YamlNodeReaderTest : DescribeSpec({
                 - *thing
             """.trimIndent()
 
             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
                         YamlList(
                             listOf(
@@ -361,25 +361,40 @@ class YamlNodeReaderTest : DescribeSpec({
             val input = """
                 - thing2
                 - *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 {
                         it.message shouldBe "Unknown anchor 'thing'."
                         it.line shouldBe 2
                         it.column shouldBe 3
                         it.path shouldBe YamlPath.root.withListEntry(1, Location(2, 3)).withError(Location(2, 3))
                     }
                 }
             }
+
+            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") {
             val input = """[thing1, thing2, "thing3", 'thing4', "thing\"5"]"""
 
@@ -650,11 +665,11 @@ class YamlNodeReaderTest : DescribeSpec({
                 key2: *value
             """.trimIndent()
 
             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))
                 val value2Path = key2Path.withMapElementValue(Location(2, 7)).withAliasReference("value", Location(2, 7)).withAliasDefinition("value", Location(1, 7))
 
@@ -1154,11 +1169,11 @@ class YamlNodeReaderTest : DescribeSpec({
                   label: center/big
             """.trimIndent()
 
             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))
                 val firstYPath = firstItemPath.withMapElementKey("y", Location(1, 19))
                 val secondItemPath = YamlPath.root.withListEntry(1, Location(3, 3))
@@ -1204,11 +1219,11 @@ class YamlNodeReaderTest : DescribeSpec({
                   label: center/big
             """.trimIndent()
 
             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))
                 val firstYPath = firstItemPath.withMapElementKey("y", Location(1, 19))
                 val secondItemPath = YamlPath.root.withListEntry(1, Location(3, 3))
@@ -1300,11 +1315,11 @@ class YamlNodeReaderTest : DescribeSpec({
                   label: center/big
             """.trimIndent()
 
             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))
                 val firstYPath = firstItemPath.withMapElementKey("y", Location(1, 19))
 
@@ -1363,11 +1378,11 @@ class YamlNodeReaderTest : DescribeSpec({
                   label: center/big
             """.trimIndent()
 
             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))
                 val firstYPath = firstItemPath.withMapElementKey("y", Location(1, 17))
 
@@ -1529,11 +1544,11 @@ class YamlNodeReaderTest : DescribeSpec({
                     baz: *extension
             """.trimIndent()
 
             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))
                 val barKeyPath = fooValuePath.withMapElementKey("bar", Location(4, 5))
                 val bazKeyPath = fooValuePath.withMapElementKey("baz", Location(5, 5))
@@ -1596,11 +1611,11 @@ class YamlNodeReaderTest : DescribeSpec({
                 << : [ *extension ]
             """.trimIndent()
 
             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))
                     .withListEntry(0, Location(4, 8))
                     .withAliasReference("extension", Location(4, 8))

Vulnerable code:                 context("parsing that input") {
                    val configuration = YamlConfiguration(extensionDefinitionPrefix = ".")
        val reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix)
                val result = YamlNodeReader(parser).read()
                val result = YamlNodeReader(parser).read()
            describe("parsing that input") {
                        YamlNodeReader(parser).read()
                val result = YamlNodeReader(parser).read()
                val result = YamlNodeReader(parser).read()
                val result = YamlNodeReader(parser).read()
                val result = YamlNodeReader(parser).read()
                val result = YamlNodeReader(parser).read()
                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".").read()
                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".").read()
Fixed code:  * * [allowAnchorsAndAliases]: set to true to allow anchors and aliases when decoding YAML (defaults to `false`)
    internal val allowAnchorsAndAliases: Boolean = false,

public class ForbiddenAnchorOrAliasException(message: String, path: YamlPath) : YamlException(message, path)
                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 reader = YamlNodeReader(parser, configuration.extensionDefinitionPrefix, configuration.allowAnchorsAndAliases)
    private val allowAnchorsAndAliases: Boolean = false,
                if (!allowAnchorsAndAliases) {
                    throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path)
                }

        if (!allowAnchorsAndAliases) {
            throw ForbiddenAnchorOrAliasException("Parsing anchors and aliases is disabled.", path)
        }

                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
            describe("parsing that input with anchor and alias parsing enabled") {
                        YamlNodeReader(parser, allowAnchorsAndAliases = true).read()

            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
                    }
                }
            }
                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                val result = YamlNodeReader(parser, allowAnchorsAndAliases = true).read()
                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".", allowAnchorsAndAliases = true).read()
                val result = YamlNodeReader(parser, extensionDefinitionPrefix = ".", allowAnchorsAndAliases = true).read()