Skip to content

Commit fce83c9

Browse files
mechkoclaude
andcommitted
#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 <[email protected]>
1 parent 90563eb commit fce83c9

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/main/java/org/json/JSONObject.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,7 +1399,15 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) {
13991399
return (BigInteger) val;
14001400
}
14011401
if (val instanceof BigDecimal){
1402-
return ((BigDecimal) val).toBigInteger();
1402+
BigDecimal bd = (BigDecimal) val;
1403+
// Same ceiling as the parse-time maxNumberLength guard: refuse to
1404+
// materialise an integer whose decimal representation would exceed
1405+
// DEFAULT_MAX_NUMBER_LENGTH digits. Prevents DoS via short exponent
1406+
// literals like 1e100000000 (CVE-2026-59171, see issue #1063).
1407+
if ((long) bd.precision() - bd.scale() > ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH) {
1408+
return defaultValue;
1409+
}
1410+
return bd.toBigInteger();
14031411
}
14041412
if (val instanceof Double || val instanceof Float){
14051413
if (!numberIsFinite((Number)val)) {
@@ -1422,7 +1430,11 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) {
14221430
*/
14231431
final String valStr = val.toString();
14241432
if(isDecimalNotation(valStr)) {
1425-
return new BigDecimal(valStr).toBigInteger();
1433+
BigDecimal bd = new BigDecimal(valStr);
1434+
if ((long) bd.precision() - bd.scale() > ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH) {
1435+
return defaultValue;
1436+
}
1437+
return bd.toBigInteger();
14261438
}
14271439
return new BigInteger(valStr);
14281440
} catch (Exception e) {

src/test/java/org/json/junit/JSONObjectTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,43 @@ public void bigNumberOperations() {
13671367
Util.checkJSONArrayMaps(jsonArray1, jsonObject0.getMapType());
13681368
}
13691369

1370+
/**
1371+
* Verifies that getBigInteger / optBigInteger do not attempt to materialise a
1372+
* BigInteger whose decimal representation would exceed
1373+
* ParserConfiguration.DEFAULT_MAX_NUMBER_LENGTH digits. A short exponent-notation
1374+
* literal such as 1e100000000 is stored compactly as a BigDecimal at parse time
1375+
* but would otherwise expand to ~100 000 000 digits in BigDecimal.toBigInteger(),
1376+
* stalling the thread / OOM (CVE-2026-59171, issue #1063).
1377+
*/
1378+
@Test(timeout = 5000)
1379+
public void getBigIntegerHugeExponentReturnsDefault() {
1380+
// BigDecimal path: value arrives via the parser as a BigDecimal
1381+
JSONObject jo = new JSONObject("{\"x\":1e100000000}");
1382+
assertTrue("huge-exponent literal parses to BigDecimal", jo.get("x") instanceof BigDecimal);
1383+
assertNull("optBigInteger returns default for huge exponent", jo.optBigInteger("x", null));
1384+
try {
1385+
jo.getBigInteger("x");
1386+
fail("getBigInteger should throw for huge exponent");
1387+
} catch (JSONException expected) {
1388+
}
1389+
1390+
// String path: value put() as a String, exercised via objectToBigInteger's
1391+
// isDecimalNotation branch
1392+
JSONObject jo2 = new JSONObject();
1393+
jo2.put("x", "1e100000000");
1394+
assertNull("optBigInteger returns default for huge-exponent string", jo2.optBigInteger("x", null));
1395+
1396+
// JSONArray accessors delegate to the same helper
1397+
JSONArray ja = new JSONArray("[1e100000000]");
1398+
assertTrue("optBigInteger returns default for huge exponent (array)",
1399+
BigInteger.ONE.equals(ja.optBigInteger(0, BigInteger.ONE)));
1400+
1401+
// Boundary: a value at the limit still converts correctly
1402+
JSONObject jo3 = new JSONObject("{\"x\":1e999}");
1403+
assertEquals("1e999 still converts", 0,
1404+
jo3.getBigInteger("x").compareTo(BigInteger.TEN.pow(999)));
1405+
}
1406+
13701407
/**
13711408
* The purpose for the static method getNames() methods are not clear. This
13721409
* method is not called from within JSON-Java. Most likely uses are to prep

0 commit comments

Comments
 (0)