From 90563eb8777e35e0ddf95c7a67a75102d26b04a4 Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Mon, 13 Jul 2026 15:33:12 -0500 Subject: [PATCH 1/5] 1063-option-3 parserConfig set max len --- src/main/java/org/json/JSONObject.java | 32 ++++- src/main/java/org/json/JSONTokener.java | 2 +- .../java/org/json/ParserConfiguration.java | 62 ++++++++- src/main/java/org/json/XML.java | 26 +++- .../java/org/json/junit/JSONObjectTest.java | 130 +++++++++++------- 5 files changed, 193 insertions(+), 59 deletions(-) diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 2471aa037..cb7d1a7e0 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -2667,16 +2667,33 @@ protected static boolean isDecimalNotation(final String val) { /** * Try to convert a string into a number, boolean, or null. If the string * can't be converted, return the string. + * Warning! stringToValue(String) uses the default max number length. If you want to override it, + * use a suitable initialized JSONParserConfiguration and the method: stringToValue(String, JSONParserConfiguration). * - * @param string - * A String. can not be null. + * @param str A String. can not be null. * @return A simple JSON value. * @throws NullPointerException * Thrown if the string is null. */ // Changes to this method must be copied to the corresponding method in // the XML class to keep full support for Android - public static Object stringToValue(String string) { + public static Object stringToValue(String str) { + return stringToValue(str, new JSONParserConfiguration()); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string A String. can not be null. + * @param jsonParserConfiguration the parser config + * @return A simple JSON value. If the string represents a number that is too large, + * a string will be returned. + * @throws NullPointerException Thrown if the string is null. + */ + // Changes to this method must be copied to the corresponding method in + // the XML class to keep full support for Android + public static Object stringToValue(String string, JSONParserConfiguration jsonParserConfiguration) { if ("".equals(string)) { return string; } @@ -2700,7 +2717,14 @@ public static Object stringToValue(String string) { char initial = string.charAt(0); if ((initial >= '0' && initial <= '9') || initial == '-') { try { - if (string.length() <= 1000) { + if (jsonParserConfiguration == null) { + jsonParserConfiguration = new JSONParserConfiguration(); + } + // user declines max number checking + if (jsonParserConfiguration.getMaxNumberLength() == JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + return stringToNumber(string); + } + if (string.length() <= jsonParserConfiguration.getMaxNumberLength()) { return stringToNumber(string); } } catch (Exception ignore) { diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index 07ff18c99..3726856d3 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -513,7 +513,7 @@ Object nextSimpleValue(char c) { jsonParserConfiguration.isStrictMode() && string.endsWith(".")) { throw this.syntaxError(String.format("Strict mode error: Value '%s' ends with dot", string)); } - Object obj = JSONObject.stringToValue(string); + Object obj = JSONObject.stringToValue(string, jsonParserConfiguration); // if obj is a boolean, look at string if (jsonParserConfiguration != null && jsonParserConfiguration.isStrictMode()) { diff --git a/src/main/java/org/json/ParserConfiguration.java b/src/main/java/org/json/ParserConfiguration.java index 06cc44366..df700b355 100644 --- a/src/main/java/org/json/ParserConfiguration.java +++ b/src/main/java/org/json/ParserConfiguration.java @@ -13,11 +13,21 @@ public class ParserConfiguration { */ public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1; + /** + * Used to indicate there's no defined limit to the maximum number length + */ + public static final int UNDEFINED_MAXIMUM_NUMBER_LENGTH = -1; + /** * The default maximum nesting depth when parsing a document. */ public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; + /** + * The default max number length + */ + public static final int DEFAULT_MAX_NUMBER_LENGTH = 1000; + /** * Specifies if values should be kept as strings (true), or if * they should try to be guessed into JSON values (numeric, boolean, string). @@ -29,23 +39,31 @@ public class ParserConfiguration { */ protected int maxNestingDepth; + /** + * The max number of chars for any number. Exceeding this limit will cause the value to be converted to a string + */ + protected int maxNumberLength; + /** * Constructs a new ParserConfiguration with default settings. */ public ParserConfiguration() { this.keepStrings = false; this.maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH; + this.maxNumberLength = DEFAULT_MAX_NUMBER_LENGTH; } /** - * Constructs a new ParserConfiguration with the specified settings. + * Constructs a new ParserConfiguration with the specified settings. Use the with* methods instead of calling this ctor. * * @param keepStrings A boolean indicating whether to preserve strings during parsing. * @param maxNestingDepth An integer representing the maximum allowed nesting depth. + * @deprecated */ protected ParserConfiguration(final boolean keepStrings, final int maxNestingDepth) { this.keepStrings = keepStrings; this.maxNestingDepth = maxNestingDepth; + this.maxNumberLength = DEFAULT_MAX_NUMBER_LENGTH; } /** @@ -58,10 +76,11 @@ protected ParserConfiguration clone() { // item, a new map instance should be created and if possible each value in the // map should be cloned as well. If the values of the map are known to also // be immutable, then a shallow clone of the map is acceptable. - return new ParserConfiguration( - this.keepStrings, - this.maxNestingDepth - ); + ParserConfiguration parserConfiguration = new ParserConfiguration(); + parserConfiguration.keepStrings = this.keepStrings; + parserConfiguration.maxNestingDepth = this.maxNestingDepth; + parserConfiguration.maxNumberLength = this.maxNumberLength; + return parserConfiguration; } /** @@ -123,4 +142,37 @@ public T withMaxNestingDepth(int maxNestingDepth return newConfig; } + + + /** + * The maximum number length that the parser will allow + * + * @return the maximum number lengtj set for this configuration + */ + public int getMaxNumberLength() { + return maxNumberLength; + } + + /** + * Defines the maximum number length that the parser will allow + * Using any negative value as a parameter is equivalent to setting no limit to the length + * which means any size number is allowed + * + * @param maxNumberLength the maximum number length allowed + * @param the type of the configuration object + * @return The existing configuration will not be modified. A new configuration is returned. + */ + @SuppressWarnings("unchecked") + public T withMaxNumberLength(int maxNumberLength) { + T newConfig = (T) this.clone(); + + if (maxNumberLength > UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + newConfig.maxNumberLength = maxNumberLength; + } else { + newConfig.maxNumberLength = UNDEFINED_MAXIMUM_NUMBER_LENGTH; + } + + return newConfig; + } + } diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 716b6d647..195da8d5d 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -617,16 +617,32 @@ public static Object stringToValue(String string, XMLXsiTypeConverter typeCon return stringToValue(string); } + /** + * This method is the same as {@link JSONObject#stringToValue(String)}. + * Warning! stringToValue(String) uses the default max number length. If you want to override it, + * use a suitable initialized XMLParserConfiguration and the method: stringToValue(String, XMLParserConfiguration). + * @param str String to convert + * @return JSON value of this string or the string + */ + // To maintain compatibility with the Android API, this method is a direct copy of + // the one in JSONObject. Changes made here should be reflected there. + // This method should not make calls out of the XML object. + public static Object stringToValue(String str) { + return stringToValue(str, new XMLParserConfiguration()); + } + /** * This method is the same as {@link JSONObject#stringToValue(String)}. * * @param string String to convert - * @return JSON value of this string or the string + * @param xmlParserConfiguration the XML parser config object + * @return JSON value of this string or the string. If the string represents a number that is too large, + * a string will be returned. */ // To maintain compatibility with the Android API, this method is a direct copy of // the one in JSONObject. Changes made here should be reflected there. // This method should not make calls out of the XML object. - public static Object stringToValue(String string) { + public static Object stringToValue(String string, XMLParserConfiguration xmlParserConfiguration) { if ("".equals(string)) { return string; } @@ -650,7 +666,11 @@ public static Object stringToValue(String string) { char initial = string.charAt(0); if ((initial >= '0' && initial <= '9') || initial == '-') { try { - if(string.length() <= 1000) { + // user declines max number checking + if (xmlParserConfiguration.getMaxNumberLength() == JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + return stringToNumber(string); + } + if(string.length() <= xmlParserConfiguration.getMaxNumberLength()) { return stringToNumber(string); } } catch (Exception ignore) { diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index 6762cf071..4a32b602d 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -67,7 +67,6 @@ import org.json.junit.data.CustomClassH; import org.json.junit.data.CustomClassI; import org.json.junit.data.CustomClassJ; -import org.json.JSONObject; import org.junit.After; import org.junit.Ignore; import org.junit.Test; @@ -3801,33 +3800,84 @@ public void testMaxNumberLength() { sb.append("9999999999"); } - // edge case: JSONObject with number just under the max length limit - String s1Object = "{ \"a\": " + sb + "}"; - JSONObject jsonObject1 = new JSONObject(s1Object); - Object obj1Object = jsonObject1.get("a"); - assertTrue(obj1Object instanceof Number); - assertEquals(1000, obj1Object.toString().length()); - - // edge case: JSONArray with number just under the max length limit - String s1Array = "[" + sb + "]"; - JSONArray jsonArray1 = new JSONArray(s1Array); - Object obj1Array = jsonArray1.get(0); - assertTrue(obj1Array instanceof Number); - assertEquals(1000, obj1Array.toString().length()); - - // edge case: JSONObject with number just over the max length limit - String s2Object = "{ \"a\": " + sb + "9}"; - JSONObject jsonObject2 = new JSONObject(s2Object); - Object obj2Object = jsonObject2.get("a"); - assertTrue(obj2Object instanceof String); - assertEquals(1001, ((String) obj2Object).length()); - - // edge case: JSONArray with number just over the max length limit - String s2Array = "[" + sb + "9]"; - JSONArray jsonArray2 = new JSONArray(s2Array); - Object obj2Array = jsonArray2.get(0); - assertTrue(obj2Array instanceof String); - assertEquals(1001, ((String) obj2Array).length()); + // JSONObject with number just under the max length limit + checkJSONObjectMaxLen(sb.toString(), true, null); + + // JSONArray with number just under the max length limit + checkJSONArrayMaxLen(sb.toString(), true, null); + + // JSONObject with number just over the max length limit + checkJSONObjectMaxLen(sb + "9", false, null); + + // JSONArray with number just over the max length limit + checkJSONArrayMaxLen(sb + "9", false, null); + + // JSONObject with number at config max length limit + checkJSONObjectMaxLen(sb.toString() + sb.toString(), true, new JSONParserConfiguration().withMaxNumberLength(2000)); + + // JSONArray with number at config max length limit + checkJSONArrayMaxLen((sb.toString() + sb), true, new JSONParserConfiguration().withMaxNumberLength(2000)); + + // JSONObject with number just over config max length limit + checkJSONObjectMaxLen(sb.toString() + sb.toString() + "9", false, new JSONParserConfiguration().withMaxNumberLength(2000)); + + // JSONArray with number just over config max length limit + checkJSONArrayMaxLen((sb.toString() + sb + "9"), false, new JSONParserConfiguration().withMaxNumberLength(2000)); + + // JSONObject with large number, no checks + checkJSONObjectMaxLen(sb.toString() + sb.toString() + "9", true, + new JSONParserConfiguration().withMaxNumberLength(JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH)); + + // JSONArray with number just over config max length limit + checkJSONArrayMaxLen((sb.toString() + sb + "9"), true, + new JSONParserConfiguration().withMaxNumberLength(JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH)); + } + + /** + * Convenience method to check JSONObject for a long number + * @param s string containing the number digits to check + * @param isValid true if number is valid, otherwise false + * @param jsonParserConfiguration the config object + */ + private static void checkJSONObjectMaxLen(String s, boolean isValid, JSONParserConfiguration jsonParserConfiguration) { + String str = "{ \"a\": " + s + "}"; + JSONObject jsonObject; + if (jsonParserConfiguration == null) { + jsonObject = new JSONObject(str); + } else { + jsonObject = new JSONObject(str, jsonParserConfiguration); + } + Object obj = jsonObject.get("a"); + if (isValid) { + assertTrue(obj instanceof Number); + } else { + assertTrue(obj instanceof String); + } + // may not work for scientific notation and BigDecimal + // assertEquals(s.length(), obj.toString().length()); + } + + /** + * Convenience method to check JSONArray for a long number + * @param s string containing the number digits to check + * @param isValid true if number is valid, otherwise false + */ + private static void checkJSONArrayMaxLen(String s, boolean isValid, JSONParserConfiguration jsonParserConfiguration) { + String str = "[" + s + "]"; + JSONArray jsonArray; + if (jsonParserConfiguration == null) { + jsonArray = new JSONArray(str); + } else { + jsonArray = new JSONArray(str, jsonParserConfiguration); + } + Object obj = jsonArray.get(0); + if (isValid) { + assertTrue(obj instanceof Number); + } else { + assertTrue(obj instanceof String); + } + // may not work for scientific notation and BigDecimal + // assertEquals(s.length(), obj.toString().length()); } /** @@ -3843,16 +3893,12 @@ public void testMaxNumberLengthNegativeInteger() { assertEquals(1000, sb.length()); // at max length: parsed as number - JSONObject jo = new JSONObject("{ \"a\": " + sb + "}"); - Object val = jo.get("a"); - assertTrue("Expected Number but got " + val.getClass(), val instanceof Number); + checkJSONObjectMaxLen(sb.toString(), true, null); // over max length: returned as string sb.append("1"); assertEquals(1001, sb.length()); - JSONObject jo2 = new JSONObject("{ \"a\": " + sb + "}"); - Object val2 = jo2.get("a"); - assertTrue("Expected String but got " + val2.getClass(), val2 instanceof String); + checkJSONObjectMaxLen(sb.toString(), false, null); } /** @@ -3873,16 +3919,12 @@ public void testMaxNumberLengthDecimal() { assertEquals(1000, sb.length()); // at max length: parsed as number (BigDecimal) - JSONObject jo = new JSONObject("{ \"a\": " + sb + "}"); - Object val = jo.get("a"); - assertTrue("Expected Number but got " + val.getClass(), val instanceof Number); + checkJSONObjectMaxLen(sb.toString(), true, null); // over max length: returned as string sb.append("3"); assertEquals(1001, sb.length()); - JSONObject jo2 = new JSONObject("{ \"a\": " + sb + "}"); - Object val2 = jo2.get("a"); - assertTrue("Expected String but got " + val2.getClass(), val2 instanceof String); + checkJSONObjectMaxLen(sb.toString(), false, null); } /** @@ -3899,9 +3941,7 @@ public void testMaxNumberLengthScientificNotation() { assertEquals(1000, sb.length()); // at max length: parsed as number - JSONObject jo = new JSONObject("{ \"a\": " + sb + "}"); - Object val = jo.get("a"); - assertTrue("Expected Number but got " + val.getClass(), val instanceof Number); + checkJSONObjectMaxLen(sb.toString(), true, null); // over max length: returned as string sb = new StringBuilder("1."); @@ -3910,9 +3950,7 @@ public void testMaxNumberLengthScientificNotation() { } sb.append("e100"); assertEquals(1001, sb.length()); - JSONObject jo2 = new JSONObject("{ \"a\": " + sb + "}"); - Object val2 = jo2.get("a"); - assertTrue("Expected String but got " + val2.getClass(), val2 instanceof String); + checkJSONObjectMaxLen(sb.toString(), false, null); } /** From fce83c91bb229dde472283dea648a9a8955237fd Mon Sep 17 00:00:00 2001 From: Mirko Swillus Date: Tue, 14 Jul 2026 11:59:27 +0200 Subject: [PATCH 2/5] #1063: bound BigDecimal->BigInteger expansion in objectToBigInteger Completes the CVE-2026-59171 fix started in ab92bb9 / #1065. The 1000-char length guard in stringToValue admits short exponent-notation literals (e.g. 1e100000000, 11 chars) which are stored compactly as BigDecimal and only expand when getBigInteger/optBigInteger calls BigDecimal.toBigInteger(), materialising ~10^8 digits and stalling the thread or throwing OOM. Guard both toBigInteger() sites in objectToBigInteger by rejecting any BigDecimal whose integer part would exceed ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH decimal digits (precision() - scale(), both O(1) reads). Returns defaultValue on overflow, matching the method's existing behaviour for non-finite and unparseable values. Covers JSONObject.getBigInteger/optBigInteger and JSONArray.getBigInteger/optBigInteger (all delegate to this helper). Adds JSONObjectTest.getBigIntegerHugeExponentReturnsDefault with a 5s timeout so a regression fails fast rather than hanging CI. Co-Authored-By: Claude --- src/main/java/org/json/JSONObject.java | 16 +++++++- .../java/org/json/junit/JSONObjectTest.java | 37 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index cb7d1a7e0..908cc6996 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -1399,7 +1399,15 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { return (BigInteger) val; } if (val instanceof BigDecimal){ - return ((BigDecimal) val).toBigInteger(); + BigDecimal bd = (BigDecimal) val; + // Same ceiling as the parse-time maxNumberLength guard: refuse to + // materialise an integer whose decimal representation would exceed + // DEFAULT_MAX_NUMBER_LENGTH digits. Prevents DoS via short exponent + // literals like 1e100000000 (CVE-2026-59171, see issue #1063). + if ((long) bd.precision() - bd.scale() > ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH) { + return defaultValue; + } + return bd.toBigInteger(); } if (val instanceof Double || val instanceof Float){ if (!numberIsFinite((Number)val)) { @@ -1422,7 +1430,11 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { */ final String valStr = val.toString(); if(isDecimalNotation(valStr)) { - return new BigDecimal(valStr).toBigInteger(); + BigDecimal bd = new BigDecimal(valStr); + if ((long) bd.precision() - bd.scale() > ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH) { + return defaultValue; + } + return bd.toBigInteger(); } return new BigInteger(valStr); } catch (Exception e) { diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index 4a32b602d..a06c3dd44 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -1367,6 +1367,43 @@ public void bigNumberOperations() { Util.checkJSONArrayMaps(jsonArray1, jsonObject0.getMapType()); } + /** + * Verifies that getBigInteger / optBigInteger do not attempt to materialise a + * BigInteger whose decimal representation would exceed + * ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH digits. A short exponent-notation + * literal such as 1e100000000 is stored compactly as a BigDecimal at parse time + * but would otherwise expand to ~100 000 000 digits in BigDecimal.toBigInteger(), + * stalling the thread / OOM (CVE-2026-59171, issue #1063). + */ + @Test(timeout = 5000) + public void getBigIntegerHugeExponentReturnsDefault() { + // BigDecimal path: value arrives via the parser as a BigDecimal + JSONObject jo = new JSONObject("{\"x\":1e100000000}"); + assertTrue("huge-exponent literal parses to BigDecimal", jo.get("x") instanceof BigDecimal); + assertNull("optBigInteger returns default for huge exponent", jo.optBigInteger("x", null)); + try { + jo.getBigInteger("x"); + fail("getBigInteger should throw for huge exponent"); + } catch (JSONException expected) { + } + + // String path: value put() as a String, exercised via objectToBigInteger's + // isDecimalNotation branch + JSONObject jo2 = new JSONObject(); + jo2.put("x", "1e100000000"); + assertNull("optBigInteger returns default for huge-exponent string", jo2.optBigInteger("x", null)); + + // JSONArray accessors delegate to the same helper + JSONArray ja = new JSONArray("[1e100000000]"); + assertTrue("optBigInteger returns default for huge exponent (array)", + BigInteger.ONE.equals(ja.optBigInteger(0, BigInteger.ONE))); + + // Boundary: a value at the limit still converts correctly + JSONObject jo3 = new JSONObject("{\"x\":1e999}"); + assertEquals("1e999 still converts", 0, + jo3.getBigInteger("x").compareTo(BigInteger.TEN.pow(999))); + } + /** * The purpose for the static method getNames() methods are not clear. This * method is not called from within JSON-Java. Most likely uses are to prep From e40d933d50f085c983cce5c377c3cad75216ef6b Mon Sep 17 00:00:00 2001 From: Mirko Swillus Date: Tue, 14 Jul 2026 13:36:46 +0200 Subject: [PATCH 3/5] #1063: add comment to expected-exception catch block (SonarCloud java:S108) Co-Authored-By: Claude --- src/test/java/org/json/junit/JSONObjectTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index a06c3dd44..5ac4643d8 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -1385,6 +1385,7 @@ public void getBigIntegerHugeExponentReturnsDefault() { jo.getBigInteger("x"); fail("getBigInteger should throw for huge exponent"); } catch (JSONException expected) { + // expected: integer part exceeds DEFAULT_MAX_NUMBER_LENGTH digits } // String path: value put() as a String, exercised via objectToBigInteger's From 703ac34a55400f9c50889e3d0c778b6d7962e81c Mon Sep 17 00:00:00 2001 From: Mirko Swillus Date: Thu, 16 Jul 2026 11:42:40 +0200 Subject: [PATCH 4/5] #1063: add JSONParserConfiguration overloads for getBigInteger/optBigInteger Per review on #1067: - objectToBigInteger(val, dflt, JSONParserConfiguration) uses cfg.getMaxNumberLength() for the digit-count guard; -1 disables it. Existing 2-arg form delegates with a default config. - New public overloads on JSONObject and JSONArray: getBigInteger(key, cfg) / optBigInteger(key, dflt, cfg). Existing methods delegate with a default config. - objectToBigDecimal left unchanged (no expansion path; agreed on PR). - Tests cover default (1000), raised (2000), lowered (5), disabled (-1), null config, and JSONArray overloads. Co-Authored-By: Claude --- src/main/java/org/json/JSONArray.java | 52 ++++++++++++- src/main/java/org/json/JSONObject.java | 78 +++++++++++++++++-- .../java/org/json/junit/JSONObjectTest.java | 52 +++++++++++++ 3 files changed, 172 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index d1dcf5c44..0d7fde9df 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -483,8 +483,29 @@ public BigDecimal getBigDecimal (int index) throws JSONException { * to a BigInteger. */ public BigInteger getBigInteger (int index) throws JSONException { + return this.getBigInteger(index, new JSONParserConfiguration()); + } + + /** + * Get the BigInteger value associated with an index. + * + * @param index + * The index must be between 0 and length() - 1. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible. Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return The value. + * @throws JSONException + * If the key is not found or if the value cannot be converted + * to a BigInteger. + */ + public BigInteger getBigInteger (int index, JSONParserConfiguration jsonParserConfiguration) + throws JSONException { Object object = this.get(index); - BigInteger val = JSONObject.objectToBigInteger(object, null); + BigInteger val = JSONObject.objectToBigInteger(object, null, jsonParserConfiguration); if(val == null) { throw wrongValueFormatException(index, "BigInteger", object, null); } @@ -960,8 +981,8 @@ public > E optEnum(Class clazz, int index, E defaultValue) } /** - * Get the optional BigInteger value associated with an index. The - * defaultValue is returned if there is no value for the index, or if the + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the * value is not a number and cannot be converted to a number. * * @param index @@ -971,8 +992,31 @@ public > E optEnum(Class clazz, int index, E defaultValue) * @return The value. */ public BigInteger optBigInteger(int index, BigInteger defaultValue) { + return this.optBigInteger(index, defaultValue, new JSONParserConfiguration()); + } + + /** + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the + * value is not a number and cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default value. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible and {@code defaultValue} is returned. + * Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return The value. + */ + public BigInteger optBigInteger(int index, BigInteger defaultValue, + JSONParserConfiguration jsonParserConfiguration) { Object val = this.opt(index); - return JSONObject.objectToBigInteger(val, defaultValue); + return JSONObject.objectToBigInteger(val, defaultValue, jsonParserConfiguration); } /** diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index 908cc6996..bf9f92007 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -760,8 +760,29 @@ public boolean getBoolean(String key) throws JSONException { * be converted to BigInteger. */ public BigInteger getBigInteger(String key) throws JSONException { + return this.getBigInteger(key, new JSONParserConfiguration()); + } + + /** + * Get the BigInteger value associated with a key. + * + * @param key + * A key string. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible. Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value cannot + * be converted to BigInteger. + */ + public BigInteger getBigInteger(String key, JSONParserConfiguration jsonParserConfiguration) + throws JSONException { Object object = this.get(key); - BigInteger ret = objectToBigInteger(object, null); + BigInteger ret = objectToBigInteger(object, null, jsonParserConfiguration); if (ret != null) { return ret; } @@ -1381,8 +1402,31 @@ static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue, boolea * @return An object which is the value. */ public BigInteger optBigInteger(String key, BigInteger defaultValue) { + return this.optBigInteger(key, defaultValue, new JSONParserConfiguration()); + } + + /** + * Get an optional BigInteger associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @param jsonParserConfiguration + * A configuration whose {@code maxNumberLength} bounds the number of + * decimal digits in the returned integer. Values exceeding this length + * are treated as unconvertible and {@code defaultValue} is returned. + * Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable + * this check. + * @return An object which is the value. + */ + public BigInteger optBigInteger(String key, BigInteger defaultValue, + JSONParserConfiguration jsonParserConfiguration) { Object val = this.opt(key); - return objectToBigInteger(val, defaultValue); + return objectToBigInteger(val, defaultValue, jsonParserConfiguration); } /** @@ -1392,9 +1436,29 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) { * to convert. */ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { + return objectToBigInteger(val, defaultValue, new JSONParserConfiguration()); + } + + /** + * @param val value to convert + * @param defaultValue default value to return is the conversion doesn't work or is null. + * @param jsonParserConfiguration parser configuration whose {@code maxNumberLength} + * bounds the number of decimal digits in the resulting integer. Values whose + * integer part would exceed this length are treated as unconvertible and + * {@code defaultValue} is returned. Pass a configuration with + * {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable this check. + * @return BigInteger conversion of the original value, or the defaultValue if unable + * to convert. + */ + static BigInteger objectToBigInteger(Object val, BigInteger defaultValue, + JSONParserConfiguration jsonParserConfiguration) { if (NULL.equals(val)) { return defaultValue; } + if (jsonParserConfiguration == null) { + jsonParserConfiguration = new JSONParserConfiguration(); + } + final int maxNumberLength = jsonParserConfiguration.getMaxNumberLength(); if (val instanceof BigInteger){ return (BigInteger) val; } @@ -1402,9 +1466,10 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { BigDecimal bd = (BigDecimal) val; // Same ceiling as the parse-time maxNumberLength guard: refuse to // materialise an integer whose decimal representation would exceed - // DEFAULT_MAX_NUMBER_LENGTH digits. Prevents DoS via short exponent - // literals like 1e100000000 (CVE-2026-59171, see issue #1063). - if ((long) bd.precision() - bd.scale() > ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH) { + // maxNumberLength digits. Prevents DoS via short exponent literals + // like 1e100000000 (CVE-2026-59171, see issue #1063). + if (maxNumberLength != ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH + && (long) bd.precision() - bd.scale() > maxNumberLength) { return defaultValue; } return bd.toBigInteger(); @@ -1431,7 +1496,8 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) { final String valStr = val.toString(); if(isDecimalNotation(valStr)) { BigDecimal bd = new BigDecimal(valStr); - if ((long) bd.precision() - bd.scale() > ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH) { + if (maxNumberLength != ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH + && (long) bd.precision() - bd.scale() > maxNumberLength) { return defaultValue; } return bd.toBigInteger(); diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index 5ac4643d8..fabaa5a31 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -1405,6 +1405,58 @@ public void getBigIntegerHugeExponentReturnsDefault() { jo3.getBigInteger("x").compareTo(BigInteger.TEN.pow(999))); } + /** + * Verifies that the JSONParserConfiguration.maxNumberLength setting is honoured + * by the getBigInteger / optBigInteger overloads on JSONObject and JSONArray. + */ + @Test(timeout = 5000) + public void getBigIntegerHonorsMaxNumberLengthConfig() { + JSONObject jo = new JSONObject("{\"a\":1e1500,\"b\":1e2500}"); + + // Default config: DEFAULT_MAX_NUMBER_LENGTH == 1000, both rejected + assertNull("1e1500 rejected under default", jo.optBigInteger("a", null)); + assertNull("1e2500 rejected under default", jo.optBigInteger("b", null)); + + // Custom raised limit + JSONParserConfiguration cfg2000 = new JSONParserConfiguration().withMaxNumberLength(2000); + assertEquals("1e1500 accepted under maxNumberLength=2000", 0, + jo.getBigInteger("a", cfg2000).compareTo(BigInteger.TEN.pow(1500))); + assertNull("1e2500 rejected under maxNumberLength=2000", + jo.optBigInteger("b", null, cfg2000)); + try { + jo.getBigInteger("b", cfg2000); + fail("getBigInteger should throw for 1e2500 under maxNumberLength=2000"); + } catch (JSONException expected) { + // expected: integer part exceeds configured maxNumberLength + } + + // Custom lowered limit + JSONParserConfiguration cfg5 = new JSONParserConfiguration().withMaxNumberLength(5); + assertNull("1e1500 rejected under maxNumberLength=5", + jo.optBigInteger("a", null, cfg5)); + JSONObject small = new JSONObject("{\"x\":1234}"); + assertEquals("small value accepted under maxNumberLength=5", + BigInteger.valueOf(1234), small.getBigInteger("x", cfg5)); + + // Disabled: -1 turns the guard off. Use a moderate exponent so the test + // completes in a few ms while still exceeding the default limit. + JSONParserConfiguration cfgOff = new JSONParserConfiguration() + .withMaxNumberLength(ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH); + assertEquals("1e2500 accepted when maxNumberLength is disabled", 0, + jo.getBigInteger("b", cfgOff).compareTo(BigInteger.TEN.pow(2500))); + + // null config falls back to default + assertNull("null config behaves like default", jo.optBigInteger("a", null, null)); + + // JSONArray overloads follow the same rules + JSONArray ja = new JSONArray("[1e1500]"); + assertNull("array: 1e1500 rejected under default", ja.optBigInteger(0, null)); + assertEquals("array: 1e1500 accepted under maxNumberLength=2000", 0, + ja.getBigInteger(0, cfg2000).compareTo(BigInteger.TEN.pow(1500))); + assertEquals("array: 1e1500 accepted when disabled", 0, + ja.optBigInteger(0, null, cfgOff).compareTo(BigInteger.TEN.pow(1500))); + } + /** * The purpose for the static method getNames() methods are not clear. This * method is not called from within JSON-Java. Most likely uses are to prep From 9953b760502800f7871cd942a6c920c4306edf60 Mon Sep 17 00:00:00 2001 From: Sean Leary Date: Thu, 16 Jul 2026 13:23:39 -0500 Subject: [PATCH 5/5] max-number-length-config sonarqube fixes --- src/main/java/org/json/JSONObject.java | 14 +++++++++++++- src/main/java/org/json/ParserConfiguration.java | 3 ++- src/main/java/org/json/XML.java | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index bf9f92007..bcd218e5d 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -1484,6 +1484,18 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue, || val instanceof Short || val instanceof Byte){ return BigInteger.valueOf(((Number) val).longValue()); } + return attemptConversionToBigInteger(val, defaultValue, maxNumberLength); + } + + /** + * Convenience method to attempt conversion of value to BigInteger. + * Added to reduce complexity of objectToBigInteger() + * @param val the value to be converted + * @param defaultValue the default value to use if conversion is not attempted or fails + * @param maxNumberLength the max length allowed for BigIntegers + * @return the converted value, or the defaultValue + */ + private static BigInteger attemptConversionToBigInteger(Object val, BigInteger defaultValue, int maxNumberLength) { // don't check if it's a string in case of unchecked Number subclasses try { /** @@ -2799,7 +2811,7 @@ public static Object stringToValue(String string, JSONParserConfiguration jsonPa jsonParserConfiguration = new JSONParserConfiguration(); } // user declines max number checking - if (jsonParserConfiguration.getMaxNumberLength() == JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + if (jsonParserConfiguration.getMaxNumberLength() == ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { return stringToNumber(string); } if (string.length() <= jsonParserConfiguration.getMaxNumberLength()) { diff --git a/src/main/java/org/json/ParserConfiguration.java b/src/main/java/org/json/ParserConfiguration.java index df700b355..e80093cac 100644 --- a/src/main/java/org/json/ParserConfiguration.java +++ b/src/main/java/org/json/ParserConfiguration.java @@ -58,8 +58,9 @@ public ParserConfiguration() { * * @param keepStrings A boolean indicating whether to preserve strings during parsing. * @param maxNestingDepth An integer representing the maximum allowed nesting depth. - * @deprecated + * @deprecated Use the with*() methods instead */ + @Deprecated protected ParserConfiguration(final boolean keepStrings, final int maxNestingDepth) { this.keepStrings = keepStrings; this.maxNestingDepth = maxNestingDepth; diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index 195da8d5d..32475876c 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -667,7 +667,7 @@ public static Object stringToValue(String string, XMLParserConfiguration xmlPars if ((initial >= '0' && initial <= '9') || initial == '-') { try { // user declines max number checking - if (xmlParserConfiguration.getMaxNumberLength() == JSONParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { + if (xmlParserConfiguration.getMaxNumberLength() == ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) { return stringToNumber(string); } if(string.length() <= xmlParserConfiguration.getMaxNumberLength()) {