Skip to content

Commit 221a142

Browse files
Vampirekrzema12
andauthored
fix(abg): protect against billion laughs attack (#1662)
Co-authored-by: Piotr Krzemiński <[email protected]>
1 parent a4f2518 commit 221a142

File tree

3 files changed

+43
-2
lines changed

3 files changed

+43
-2
lines changed

action-binding-generator/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ version = rootProject.version
1313

1414
dependencies {
1515
implementation("com.squareup:kotlinpoet:1.18.1")
16+
implementation("it.krzeminski:snakeyaml-engine-kmp:3.0.2")
1617
implementation("com.charleskorn.kaml:kaml:0.61.0")
1718
implementation(projects.sharedInternal)
1819

action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProviding.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.github.typesafegithub.workflows.actionbindinggenerator.domain.repoName
1212
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.subName
1313
import io.github.typesafegithub.workflows.actionbindinggenerator.metadata.fetchUri
1414
import io.github.typesafegithub.workflows.actionbindinggenerator.utils.toPascalCase
15+
import it.krzeminski.snakeyaml.engine.kmp.api.Load
1516
import kotlinx.serialization.Serializable
1617
import kotlinx.serialization.decodeFromString
1718
import java.io.IOException
@@ -79,7 +80,7 @@ private fun ActionCoords.fetchTypingsForOlderVersionFromCatalog(fetchUri: (URI)
7980
} catch (e: IOException) {
8081
return null
8182
}
82-
val metadata = yaml.decodeFromString<CatalogMetadata>(metadataYml)
83+
val metadata = yaml.protectedDecodeFromString<CatalogMetadata>(metadataYml)
8384
val requestedVersionAsInt = this.version.versionToIntOrNull() ?: return null
8485
val fallbackVersion =
8586
metadata.versionsWithTypings
@@ -147,11 +148,18 @@ private inline fun <reified T> Yaml.decodeFromStringOrDefaultIfEmpty(
147148
default: T,
148149
): T =
149150
if (text.isNotBlank()) {
150-
decodeFromString(text)
151+
protectedDecodeFromString(text)
151152
} else {
152153
default
153154
}
154155

156+
private inline fun <reified T> Yaml.protectedDecodeFromString(text: String): T {
157+
// protect against billion laughs attack until
158+
// https://github.com/charleskorn/kaml/pull/620 is available
159+
Load().loadOne(text)
160+
return decodeFromString(text)
161+
}
162+
155163
private fun String.versionToInt() = this.versionToIntOrNull() ?: error("Version '$this' cannot be treated as numeric!")
156164

157165
private fun String.versionToIntOrNull() = lowercase().removePrefix("v").toIntOrNull()

action-binding-generator/src/test/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/typing/TypesProvidingTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package io.github.typesafegithub.workflows.actionbindinggenerator.typing
33
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords
44
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.CommitHash
55
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.TypingActualSource
6+
import io.kotest.assertions.throwables.shouldThrow
67
import io.kotest.core.spec.style.FunSpec
78
import io.kotest.matchers.shouldBe
9+
import it.krzeminski.snakeyaml.engine.kmp.exceptions.YamlEngineException
810
import java.io.IOException
911
import java.net.URI
1012

@@ -508,6 +510,36 @@ class TypesProvidingTest :
508510
TypingActualSource.TYPING_CATALOG,
509511
)
510512
}
513+
514+
test("billion laughs attack is prevented") {
515+
// Given
516+
val billionLaughsAttack =
517+
"""
518+
a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
519+
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
520+
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
521+
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
522+
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
523+
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
524+
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
525+
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g]
526+
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h]
527+
""".trimIndent()
528+
val fetchUri: (URI) -> String = {
529+
when (it) {
530+
URI("https://raw.githubusercontent.com/some-owner/some-name/some-hash/action-types.yml") -> billionLaughsAttack
531+
else -> throw IOException()
532+
}
533+
}
534+
val actionCoord = ActionCoords("some-owner", "some-name", "v3")
535+
536+
// Expect
537+
val exception =
538+
shouldThrow<YamlEngineException> {
539+
actionCoord.provideTypes(metadataRevision = CommitHash("some-hash"), fetchUri = fetchUri)
540+
}
541+
exception.message shouldBe "Number of aliases for non-scalar nodes exceeds the specified max=50"
542+
}
511543
}
512544

513545
test("non-numeric version is provided and metadata in typing catalog exists") {

0 commit comments

Comments
 (0)