Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions src/main/java/org/json/JSONArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -960,8 +981,8 @@ public <E extends Enum<E>> E optEnum(Class<E> 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
Expand All @@ -971,8 +992,31 @@ public <E extends Enum<E>> E optEnum(Class<E> 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);
}

/**
Expand Down
86 changes: 82 additions & 4 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -1392,14 +1436,43 @@ 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;
}
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
// 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();
}
if (val instanceof Double || val instanceof Float){
if (!numberIsFinite((Number)val)) {
Expand All @@ -1422,7 +1495,12 @@ 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 (maxNumberLength != ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH
&& (long) bd.precision() - bd.scale() > maxNumberLength) {
return defaultValue;
}
return bd.toBigInteger();
}
return new BigInteger(valStr);
} catch (Exception e) {
Expand Down
90 changes: 90 additions & 0 deletions src/test/java/org/json/junit/JSONObjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,96 @@ 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) {
// expected: integer part exceeds DEFAULT_MAX_NUMBER_LENGTH digits
}

// 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)));
}

/**
* 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
Expand Down