From fce83c91bb229dde472283dea648a9a8955237fd Mon Sep 17 00:00:00 2001 From: Mirko Swillus Date: Tue, 14 Jul 2026 11:59:27 +0200 Subject: [PATCH 1/3] #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 2/3] #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 3/3] #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