MLeap LeapFrame JSON tensor dimension-check bypass PoC (source-verified)
Proof-of-concept material for a huntr Model Format Vulnerability report
against combust/mleap
(mleap-tensor/src/main/scala/ml/combust/mleap/tensor/{Tensor,JsonSupport}.scala).
The bug, verified against the live source fetched directly today
The normal, safe construction path, Tensor.create()
(Tensor.scala:23-35), calls normalizeDimensions()
(Tensor.scala:51-64), which throws IllegalArgumentException if
normalizedDimensions.product != size (the values array's real length).
The JSON deserialization path bypasses this entirely:
// JsonSupport.scala:30-31 (current source)
implicit def mleapDenseTensorFormat[T: JsonFormat: ClassTag]: RootJsonFormat[DenseTensor[T]] =
jsonFormat[Array[T], Seq[Int], DenseTensor[T]](DenseTensor[T], "values", "dimensions")
implicit def mleapSparseTensorFormat[T: JsonFormat: ClassTag]: RootJsonFormat[SparseTensor[T]] =
jsonFormat[Seq[Seq[Int]], Array[T], Seq[Int], SparseTensor[T]](SparseTensor[T], "indices", "values", "dimensions")
These call the DenseTensor/SparseTensor case-class constructors
directly, never going through Tensor.create/normalizeDimensions - so a
tiny, internally-inconsistent JSON cell like evil_leapframe_cell.json
(dimensions claim 2,000,000,000 elements; only 1 real value/index present)
parses without any error.
The inconsistency only becomes dangerous later, when downstream code calls
.toArray/.toDense on the tensor (Tensor.scala:135,
new Array[T](dimensions.product)) - dimensions.product is computed as
Int (not Long), so the allocation attempt is capped near 2^31
elements (~17GB for Double), and many combinations overflow Int into a
negative number, raising NegativeArraySizeException rather than a truly
unbounded allocation.
Reachable from an untrusted .json LeapFrame via the public
FrameReader/DefaultFrameReader API, and unauthenticated via
JsonScoringController.scala:80 in the mleap-spring-boot serving stack.
Honest limits and duplicate risk (disclosed plainly, not hidden)
No JVM/Scala toolchain was available on this host to build/run mleap and
trigger this live - confidence rests on direct reading of the current
source for both the safe path (Tensor.create) and the bypass path
(JsonSupport's direct constructors), not a live crash.
Real, unresolved duplicate risk: combust/mleap issue #897,
"Security vulnerabilities in MLeap serving stack - requesting private
reporting channel" (opened by a third-party researcher, still open, content
kept private per their own request), states verbatim: "I've identified
multiple security vulnerabilities in the MLeap serving modules
(mleap-spring-boot, mleap-executor, mleap-grpc-server)... These include
issues that allow unauthenticated remote callers to crash the service and
influence server-side network behavior." This overlaps in target
(mleap-spring-boot, unauthenticated crash) with the reachability path
described above, but the issue's technical content is private, so overlap
with this specific bug cannot be confirmed or ruled out from the public
record. Disclosed here explicitly so the triager can check for duplication
against #897 directly.
Impact: Denial of Service (allocation failure/exception, not a heap overflow given the Int-bounded product) via an internally-inconsistent tensor cell in an untrusted LeapFrame JSON file, reachable unauthenticated through the serving stack.
Suggested fix: Route JSON deserialization through Tensor.create() (or
call normalizeDimensions/an equivalent consistency check directly) instead
of constructing DenseTensor/SparseTensor via their raw case-class
constructors.