Skip to content

Commit 80efb52

Browse files
authored
Merge pull request #1068 from stleary/max-number-length-config
Max number length config for BigInteger and BigDecimal
2 parents 1efb5f6 + 9953b76 commit 80efb52

6 files changed

Lines changed: 426 additions & 67 deletions

File tree

src/main/java/org/json/JSONArray.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,29 @@ public BigDecimal getBigDecimal (int index) throws JSONException {
483483
* to a BigInteger.
484484
*/
485485
public BigInteger getBigInteger (int index) throws JSONException {
486+
return this.getBigInteger(index, new JSONParserConfiguration());
487+
}
488+
489+
/**
490+
* Get the BigInteger value associated with an index.
491+
*
492+
* @param index
493+
* The index must be between 0 and length() - 1.
494+
* @param jsonParserConfiguration
495+
* A configuration whose {@code maxNumberLength} bounds the number of
496+
* decimal digits in the returned integer. Values exceeding this length
497+
* are treated as unconvertible. Pass a configuration with
498+
* {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable
499+
* this check.
500+
* @return The value.
501+
* @throws JSONException
502+
* If the key is not found or if the value cannot be converted
503+
* to a BigInteger.
504+
*/
505+
public BigInteger getBigInteger (int index, JSONParserConfiguration jsonParserConfiguration)
506+
throws JSONException {
486507
Object object = this.get(index);
487-
BigInteger val = JSONObject.objectToBigInteger(object, null);
508+
BigInteger val = JSONObject.objectToBigInteger(object, null, jsonParserConfiguration);
488509
if(val == null) {
489510
throw wrongValueFormatException(index, "BigInteger", object, null);
490511
}
@@ -960,8 +981,8 @@ public <E extends Enum<E>> E optEnum(Class<E> clazz, int index, E defaultValue)
960981
}
961982

962983
/**
963-
* Get the optional BigInteger value associated with an index. The
964-
* defaultValue is returned if there is no value for the index, or if the
984+
* Get the optional BigInteger value associated with an index. The
985+
* defaultValue is returned if there is no value for the index, or if the
965986
* value is not a number and cannot be converted to a number.
966987
*
967988
* @param index
@@ -971,8 +992,31 @@ public <E extends Enum<E>> E optEnum(Class<E> clazz, int index, E defaultValue)
971992
* @return The value.
972993
*/
973994
public BigInteger optBigInteger(int index, BigInteger defaultValue) {
995+
return this.optBigInteger(index, defaultValue, new JSONParserConfiguration());
996+
}
997+
998+
/**
999+
* Get the optional BigInteger value associated with an index. The
1000+
* defaultValue is returned if there is no value for the index, or if the
1001+
* value is not a number and cannot be converted to a number.
1002+
*
1003+
* @param index
1004+
* The index must be between 0 and length() - 1.
1005+
* @param defaultValue
1006+
* The default value.
1007+
* @param jsonParserConfiguration
1008+
* A configuration whose {@code maxNumberLength} bounds the number of
1009+
* decimal digits in the returned integer. Values exceeding this length
1010+
* are treated as unconvertible and {@code defaultValue} is returned.
1011+
* Pass a configuration with
1012+
* {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable
1013+
* this check.
1014+
* @return The value.
1015+
*/
1016+
public BigInteger optBigInteger(int index, BigInteger defaultValue,
1017+
JSONParserConfiguration jsonParserConfiguration) {
9741018
Object val = this.opt(index);
975-
return JSONObject.objectToBigInteger(val, defaultValue);
1019+
return JSONObject.objectToBigInteger(val, defaultValue, jsonParserConfiguration);
9761020
}
9771021

9781022
/**

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

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -760,8 +760,29 @@ public boolean getBoolean(String key) throws JSONException {
760760
* be converted to BigInteger.
761761
*/
762762
public BigInteger getBigInteger(String key) throws JSONException {
763+
return this.getBigInteger(key, new JSONParserConfiguration());
764+
}
765+
766+
/**
767+
* Get the BigInteger value associated with a key.
768+
*
769+
* @param key
770+
* A key string.
771+
* @param jsonParserConfiguration
772+
* A configuration whose {@code maxNumberLength} bounds the number of
773+
* decimal digits in the returned integer. Values exceeding this length
774+
* are treated as unconvertible. Pass a configuration with
775+
* {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable
776+
* this check.
777+
* @return The numeric value.
778+
* @throws JSONException
779+
* if the key is not found or if the value cannot
780+
* be converted to BigInteger.
781+
*/
782+
public BigInteger getBigInteger(String key, JSONParserConfiguration jsonParserConfiguration)
783+
throws JSONException {
763784
Object object = this.get(key);
764-
BigInteger ret = objectToBigInteger(object, null);
785+
BigInteger ret = objectToBigInteger(object, null, jsonParserConfiguration);
765786
if (ret != null) {
766787
return ret;
767788
}
@@ -1381,8 +1402,31 @@ static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue, boolea
13811402
* @return An object which is the value.
13821403
*/
13831404
public BigInteger optBigInteger(String key, BigInteger defaultValue) {
1405+
return this.optBigInteger(key, defaultValue, new JSONParserConfiguration());
1406+
}
1407+
1408+
/**
1409+
* Get an optional BigInteger associated with a key, or the defaultValue if
1410+
* there is no such key or if its value is not a number. If the value is a
1411+
* string, an attempt will be made to evaluate it as a number.
1412+
*
1413+
* @param key
1414+
* A key string.
1415+
* @param defaultValue
1416+
* The default.
1417+
* @param jsonParserConfiguration
1418+
* A configuration whose {@code maxNumberLength} bounds the number of
1419+
* decimal digits in the returned integer. Values exceeding this length
1420+
* are treated as unconvertible and {@code defaultValue} is returned.
1421+
* Pass a configuration with
1422+
* {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable
1423+
* this check.
1424+
* @return An object which is the value.
1425+
*/
1426+
public BigInteger optBigInteger(String key, BigInteger defaultValue,
1427+
JSONParserConfiguration jsonParserConfiguration) {
13841428
Object val = this.opt(key);
1385-
return objectToBigInteger(val, defaultValue);
1429+
return objectToBigInteger(val, defaultValue, jsonParserConfiguration);
13861430
}
13871431

13881432
/**
@@ -1392,14 +1436,43 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) {
13921436
* to convert.
13931437
*/
13941438
static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) {
1439+
return objectToBigInteger(val, defaultValue, new JSONParserConfiguration());
1440+
}
1441+
1442+
/**
1443+
* @param val value to convert
1444+
* @param defaultValue default value to return is the conversion doesn't work or is null.
1445+
* @param jsonParserConfiguration parser configuration whose {@code maxNumberLength}
1446+
* bounds the number of decimal digits in the resulting integer. Values whose
1447+
* integer part would exceed this length are treated as unconvertible and
1448+
* {@code defaultValue} is returned. Pass a configuration with
1449+
* {@link ParserConfiguration#UNDEFINED_MAXIMUM_NUMBER_LENGTH} to disable this check.
1450+
* @return BigInteger conversion of the original value, or the defaultValue if unable
1451+
* to convert.
1452+
*/
1453+
static BigInteger objectToBigInteger(Object val, BigInteger defaultValue,
1454+
JSONParserConfiguration jsonParserConfiguration) {
13951455
if (NULL.equals(val)) {
13961456
return defaultValue;
13971457
}
1458+
if (jsonParserConfiguration == null) {
1459+
jsonParserConfiguration = new JSONParserConfiguration();
1460+
}
1461+
final int maxNumberLength = jsonParserConfiguration.getMaxNumberLength();
13981462
if (val instanceof BigInteger){
13991463
return (BigInteger) val;
14001464
}
14011465
if (val instanceof BigDecimal){
1402-
return ((BigDecimal) val).toBigInteger();
1466+
BigDecimal bd = (BigDecimal) val;
1467+
// Same ceiling as the parse-time maxNumberLength guard: refuse to
1468+
// materialise an integer whose decimal representation would exceed
1469+
// maxNumberLength digits. Prevents DoS via short exponent literals
1470+
// like 1e100000000 (CVE-2026-59171, see issue #1063).
1471+
if (maxNumberLength != ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH
1472+
&& (long) bd.precision() - bd.scale() > maxNumberLength) {
1473+
return defaultValue;
1474+
}
1475+
return bd.toBigInteger();
14031476
}
14041477
if (val instanceof Double || val instanceof Float){
14051478
if (!numberIsFinite((Number)val)) {
@@ -1411,6 +1484,18 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) {
14111484
|| val instanceof Short || val instanceof Byte){
14121485
return BigInteger.valueOf(((Number) val).longValue());
14131486
}
1487+
return attemptConversionToBigInteger(val, defaultValue, maxNumberLength);
1488+
}
1489+
1490+
/**
1491+
* Convenience method to attempt conversion of value to BigInteger.
1492+
* Added to reduce complexity of objectToBigInteger()
1493+
* @param val the value to be converted
1494+
* @param defaultValue the default value to use if conversion is not attempted or fails
1495+
* @param maxNumberLength the max length allowed for BigIntegers
1496+
* @return the converted value, or the defaultValue
1497+
*/
1498+
private static BigInteger attemptConversionToBigInteger(Object val, BigInteger defaultValue, int maxNumberLength) {
14141499
// don't check if it's a string in case of unchecked Number subclasses
14151500
try {
14161501
/**
@@ -1422,7 +1507,12 @@ static BigInteger objectToBigInteger(Object val, BigInteger defaultValue) {
14221507
*/
14231508
final String valStr = val.toString();
14241509
if(isDecimalNotation(valStr)) {
1425-
return new BigDecimal(valStr).toBigInteger();
1510+
BigDecimal bd = new BigDecimal(valStr);
1511+
if (maxNumberLength != ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH
1512+
&& (long) bd.precision() - bd.scale() > maxNumberLength) {
1513+
return defaultValue;
1514+
}
1515+
return bd.toBigInteger();
14261516
}
14271517
return new BigInteger(valStr);
14281518
} catch (Exception e) {
@@ -2667,16 +2757,33 @@ protected static boolean isDecimalNotation(final String val) {
26672757
/**
26682758
* Try to convert a string into a number, boolean, or null. If the string
26692759
* can't be converted, return the string.
2760+
* Warning! stringToValue(String) uses the default max number length. If you want to override it,
2761+
* use a suitable initialized JSONParserConfiguration and the method: stringToValue(String, JSONParserConfiguration).
26702762
*
2671-
* @param string
2672-
* A String. can not be null.
2763+
* @param str A String. can not be null.
26732764
* @return A simple JSON value.
26742765
* @throws NullPointerException
26752766
* Thrown if the string is null.
26762767
*/
26772768
// Changes to this method must be copied to the corresponding method in
26782769
// the XML class to keep full support for Android
2679-
public static Object stringToValue(String string) {
2770+
public static Object stringToValue(String str) {
2771+
return stringToValue(str, new JSONParserConfiguration());
2772+
}
2773+
2774+
/**
2775+
* Try to convert a string into a number, boolean, or null. If the string
2776+
* can't be converted, return the string.
2777+
*
2778+
* @param string A String. can not be null.
2779+
* @param jsonParserConfiguration the parser config
2780+
* @return A simple JSON value. If the string represents a number that is too large,
2781+
* a string will be returned.
2782+
* @throws NullPointerException Thrown if the string is null.
2783+
*/
2784+
// Changes to this method must be copied to the corresponding method in
2785+
// the XML class to keep full support for Android
2786+
public static Object stringToValue(String string, JSONParserConfiguration jsonParserConfiguration) {
26802787
if ("".equals(string)) {
26812788
return string;
26822789
}
@@ -2700,7 +2807,14 @@ public static Object stringToValue(String string) {
27002807
char initial = string.charAt(0);
27012808
if ((initial >= '0' && initial <= '9') || initial == '-') {
27022809
try {
2703-
if (string.length() <= 1000) {
2810+
if (jsonParserConfiguration == null) {
2811+
jsonParserConfiguration = new JSONParserConfiguration();
2812+
}
2813+
// user declines max number checking
2814+
if (jsonParserConfiguration.getMaxNumberLength() == ParserConfiguration.UNDEFINED_MAXIMUM_NUMBER_LENGTH) {
2815+
return stringToNumber(string);
2816+
}
2817+
if (string.length() <= jsonParserConfiguration.getMaxNumberLength()) {
27042818
return stringToNumber(string);
27052819
}
27062820
} catch (Exception ignore) {

src/main/java/org/json/JSONTokener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ Object nextSimpleValue(char c) {
513513
jsonParserConfiguration.isStrictMode() && string.endsWith(".")) {
514514
throw this.syntaxError(String.format("Strict mode error: Value '%s' ends with dot", string));
515515
}
516-
Object obj = JSONObject.stringToValue(string);
516+
Object obj = JSONObject.stringToValue(string, jsonParserConfiguration);
517517
// if obj is a boolean, look at string
518518
if (jsonParserConfiguration != null &&
519519
jsonParserConfiguration.isStrictMode()) {

src/main/java/org/json/ParserConfiguration.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,21 @@ public class ParserConfiguration {
1313
*/
1414
public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1;
1515

16+
/**
17+
* Used to indicate there's no defined limit to the maximum number length
18+
*/
19+
public static final int UNDEFINED_MAXIMUM_NUMBER_LENGTH = -1;
20+
1621
/**
1722
* The default maximum nesting depth when parsing a document.
1823
*/
1924
public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512;
2025

26+
/**
27+
* The default max number length
28+
*/
29+
public static final int DEFAULT_MAX_NUMBER_LENGTH = 1000;
30+
2131
/**
2232
* Specifies if values should be kept as strings (<code>true</code>), or if
2333
* they should try to be guessed into JSON values (numeric, boolean, string).
@@ -29,23 +39,32 @@ public class ParserConfiguration {
2939
*/
3040
protected int maxNestingDepth;
3141

42+
/**
43+
* The max number of chars for any number. Exceeding this limit will cause the value to be converted to a string
44+
*/
45+
protected int maxNumberLength;
46+
3247
/**
3348
* Constructs a new ParserConfiguration with default settings.
3449
*/
3550
public ParserConfiguration() {
3651
this.keepStrings = false;
3752
this.maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH;
53+
this.maxNumberLength = DEFAULT_MAX_NUMBER_LENGTH;
3854
}
3955

4056
/**
41-
* Constructs a new ParserConfiguration with the specified settings.
57+
* Constructs a new ParserConfiguration with the specified settings. Use the with* methods instead of calling this ctor.
4258
*
4359
* @param keepStrings A boolean indicating whether to preserve strings during parsing.
4460
* @param maxNestingDepth An integer representing the maximum allowed nesting depth.
61+
* @deprecated Use the with*() methods instead
4562
*/
63+
@Deprecated
4664
protected ParserConfiguration(final boolean keepStrings, final int maxNestingDepth) {
4765
this.keepStrings = keepStrings;
4866
this.maxNestingDepth = maxNestingDepth;
67+
this.maxNumberLength = DEFAULT_MAX_NUMBER_LENGTH;
4968
}
5069

5170
/**
@@ -58,10 +77,11 @@ protected ParserConfiguration clone() {
5877
// item, a new map instance should be created and if possible each value in the
5978
// map should be cloned as well. If the values of the map are known to also
6079
// be immutable, then a shallow clone of the map is acceptable.
61-
return new ParserConfiguration(
62-
this.keepStrings,
63-
this.maxNestingDepth
64-
);
80+
ParserConfiguration parserConfiguration = new ParserConfiguration();
81+
parserConfiguration.keepStrings = this.keepStrings;
82+
parserConfiguration.maxNestingDepth = this.maxNestingDepth;
83+
parserConfiguration.maxNumberLength = this.maxNumberLength;
84+
return parserConfiguration;
6585
}
6686

6787
/**
@@ -123,4 +143,37 @@ public <T extends ParserConfiguration> T withMaxNestingDepth(int maxNestingDepth
123143

124144
return newConfig;
125145
}
146+
147+
148+
/**
149+
* The maximum number length that the parser will allow
150+
*
151+
* @return the maximum number lengtj set for this configuration
152+
*/
153+
public int getMaxNumberLength() {
154+
return maxNumberLength;
155+
}
156+
157+
/**
158+
* Defines the maximum number length that the parser will allow
159+
* Using any negative value as a parameter is equivalent to setting no limit to the length
160+
* which means any size number is allowed
161+
*
162+
* @param maxNumberLength the maximum number length allowed
163+
* @param <T> the type of the configuration object
164+
* @return The existing configuration will not be modified. A new configuration is returned.
165+
*/
166+
@SuppressWarnings("unchecked")
167+
public <T extends ParserConfiguration> T withMaxNumberLength(int maxNumberLength) {
168+
T newConfig = (T) this.clone();
169+
170+
if (maxNumberLength > UNDEFINED_MAXIMUM_NUMBER_LENGTH) {
171+
newConfig.maxNumberLength = maxNumberLength;
172+
} else {
173+
newConfig.maxNumberLength = UNDEFINED_MAXIMUM_NUMBER_LENGTH;
174+
}
175+
176+
return newConfig;
177+
}
178+
126179
}

0 commit comments

Comments
 (0)