CVE-2021-39194: Loop with Unreachable Exit Condition ('Infinite Loop') (CWE-835)
value: kaml is an open source implementation of the YAML format with support for kotlinx.serialization. In affected versions attackers that could provide arbitrary YAML input to an application that uses kaml could cause the application to endlessly loop while parsing the input. This could result in resource starvation and denial of service. This only affects applications that use polymorphic serialization with the default tagged polymorphism style. Applications using the property polymorphism style are not affected. YAML input for a polymorphic type that provided a tag but no value for the object would trigger the issue. Version 0.35.3 or later contain the fix for this issue.
CWE description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
CVSS v3 base score: 6.5
Published: 2021-09-07T20:15Z
Repository: https://github.com/charleskorn/kaml
Language: Kotlin
Files changed: src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt, src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
Commit date: 2021-09-05 16:26:07 +1000
Commit message: Fix issue where decoding a polymorphic type that uses the tag polymorphism style could hang if no value was provided after the tag.

Fixes #179.

Diff: diff --git a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt
index 5d2a51d..b59f8dd 100644
--- a/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt
+++ b/src/commonMain/kotlin/com/charleskorn/kaml/YamlInput.kt
@@ -28,10 +28,11 @@ import kotlinx.serialization.descriptors.SerialDescriptor
 import kotlinx.serialization.descriptors.SerialKind
 import kotlinx.serialization.descriptors.StructureKind
 import kotlinx.serialization.descriptors.elementNames
 import kotlinx.serialization.encoding.AbstractDecoder
 import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
 import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME
 import kotlinx.serialization.modules.SerializersModule
 import kotlinx.serialization.modules.SerializersModuleCollector
 import kotlin.reflect.KClass
 
@@ -166,11 +167,11 @@ private class YamlNullInput(val nullValue: YamlNode, context: SerializersModule,
     override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder = throw UnexpectedNullValueException(nullValue.path)
 
     override fun getCurrentLocation(): Location = nullValue.location
     override fun getCurrentPath(): YamlPath = nullValue.path
 
-    override fun decodeElementIndex(descriptor: SerialDescriptor): Int = 0
+    override fun decodeElementIndex(descriptor: SerialDescriptor): Int = DECODE_DONE
 }
 
 @OptIn(ExperimentalSerializationApi::class)
 private class YamlListInput(val list: YamlList, context: SerializersModule, configuration: YamlConfiguration) : YamlInput(list, context, configuration) {
     private var nextElementIndex = 0
diff --git a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
index e8e1beb..9adcff5 100644
--- a/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
+++ b/src/commonTest/kotlin/com/charleskorn/kaml/YamlReadingTest.kt
@@ -1191,10 +1191,29 @@ object YamlReadingTest : Spek({
                             expect(result).toBe(mapOf("value" to "asdfg"))
                         }
                     }
                 }
 
+                // See https://github.com/charleskorn/kaml/issues/179.
+                context("given some input where a tag is provided but no value is provided") {
+                    val input = """
+                        !<sealedString>
+                    """.trimIndent()
+
+                    context("parsing that input") {
+                        it("throws an appropriate exception") {
+                            expect({ polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) }).toThrow<MissingRequiredPropertyException> {
+                                message { toBe("Property 'value' is required but it is missing.") }
+                                line { toBe(1) }
+                                column { toBe(1) }
+                                propertyName { toBe("value") }
+                                path { toBe(YamlPath.root) }
+                            }
+                        }
+                    }
+                }
+
                 context("given some input where the value is a literal") {
                     val input = """
                         !<simpleString> "asdfg"
                     """.trimIndent()
 

Vulnerable code:     override fun decodeElementIndex(descriptor: SerialDescriptor): Int = 0
Fixed code: import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
    override fun decodeElementIndex(descriptor: SerialDescriptor): Int = DECODE_DONE
                // See https://github.com/charleskorn/kaml/issues/179.
                context("given some input where a tag is provided but no value is provided") {
                    val input = """
                        !<sealedString>
                    """.trimIndent()

                    context("parsing that input") {
                        it("throws an appropriate exception") {
                            expect({ polymorphicYaml.decodeFromString(TestSealedStructure.serializer(), input) }).toThrow<MissingRequiredPropertyException> {
                                message { toBe("Property 'value' is required but it is missing.") }
                                line { toBe(1) }
                                column { toBe(1) }
                                propertyName { toBe("value") }
                                path { toBe(YamlPath.root) }
                            }
                        }
                    }
                }