Skip to content

Commit 43eb60e

Browse files
mergify[bot]mkurz
andauthored
Avoid running out of memory when parsing heavily nested arrays or objects (#1226) (#1227)
Just like Jackson 2.15+ we restrict the maximum allowed number of nested arrays or objects (or mixed) to 1000. This default can be changed via a sys property. 1000 should be enough for most real world use cases. Note this is about OutOfMemoryError's, not about StackOverflowError's. StackOverflowError's are not a problem since we use a @tailrec optimized method. Therefore this fix is not 100% about CVE-2025-52999 (which in theory we do not run into) but just an additional precaution. (cherry picked from commit 9722c66) Co-authored-by: Matthias Kurz <[email protected]>
1 parent b17de8f commit 43eb60e

File tree

1 file changed

+67
-0
lines changed

1 file changed

+67
-0
lines changed

play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package play.api.libs.json
66

7+
import com.fasterxml.jackson.core.exc.StreamConstraintsException
8+
79
import java.math.BigInteger
810
import java.util.Calendar
911
import java.util.Date
@@ -561,5 +563,70 @@ class JsonSpec extends org.specs2.mutable.Specification {
561563
parsed.asInstanceOf[JsObject].fields.mustEqual(original.fields)
562564
Json.stringify(parsed).mustEqual(originalString)
563565
}
566+
567+
"allow parsing objects nested up to max depth" in {
568+
try {
569+
val depth = 1000
570+
Json.parse(("{\"obj\":" * depth) + "1" + ("}" * depth))
571+
} catch {
572+
case _: StackOverflowError =>
573+
ko("StackOverflowError thrown")
574+
case _: OutOfMemoryError =>
575+
ko("OutOfMemoryError thrown")
576+
}
577+
ok
578+
}
579+
580+
"disallow parsing nested objects exceeding max depth" in {
581+
val depth = 1001
582+
Json
583+
.parse(("{\"obj\":" * depth) + "1" + ("}" * depth))
584+
.must(throwA[StreamConstraintsException].like { case e: StreamConstraintsException =>
585+
e.getMessage.must(
586+
equalTo(
587+
"Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamReadConstraints.getMaxNestingDepth()`)"
588+
)
589+
)
590+
})
591+
}
592+
593+
"allow parsing heavily nested mixed arrays and objects" in {
594+
try {
595+
val depth = 1000 - 2 // in the string two open objects { are hardcoded
596+
Json.parse("{\"foo\": {\"arr\":" + ("[" * depth) + "1" + ("]" * depth) + "}}")
597+
} catch {
598+
case _: StackOverflowError =>
599+
ko("StackOverflowError thrown")
600+
case _: OutOfMemoryError =>
601+
ko("OutOfMemoryError thrown")
602+
}
603+
ok
604+
}
605+
606+
"disallow parsing heavily nested mixed arrays and objects" in {
607+
val depth = 1001 - 2 // in the string two open objects { are hardcoded
608+
Json
609+
.parse("{\"foo\": {\"arr\":" + ("[" * depth) + "1" + ("]" * depth) + "}}")
610+
.must(throwA[StreamConstraintsException].like { case e: StreamConstraintsException =>
611+
e.getMessage.must(
612+
equalTo(
613+
"Document nesting depth (1001) exceeds the maximum allowed (1000, from `StreamReadConstraints.getMaxNestingDepth()`)"
614+
)
615+
)
616+
})
617+
}
618+
}
619+
620+
"allow parsing many non-nested arrays and objects, not relevant for depth check" in {
621+
try {
622+
val repeat = 9999 // 10 thousand in total
623+
Json.parse("{\"foo\": [" + ("{\"arr\":[1]}," * repeat) + "{\"arr\":[1]}]}")
624+
} catch {
625+
case _: StackOverflowError =>
626+
ko("StackOverflowError thrown")
627+
case _: OutOfMemoryError =>
628+
ko("OutOfMemoryError thrown")
629+
}
630+
ok
564631
}
565632
}

0 commit comments

Comments
 (0)