diff --git a/README.md b/README.md
index 40acb8f06..75a5d37a1 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ JSON in Java [package org.json]
[](https://github.com/stleary/JSON-java/actions/workflows/codeql-analysis.yml)
[](https://javadoc.io/doc/org.json/json)
-**[Click here if you just want the latest release jar file.](https://search.maven.org/remotecontent?filepath=org/json/json/20260522/json-20260522.jar)**
+**[Click here if you just want the latest release jar file.](https://repo1.maven.org/maven2/org/json/json/20260719/json-20260719.jar)**
# Overview
diff --git a/build.gradle b/build.gradle
index d8b69805f..f9251816c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -42,7 +42,7 @@ subprojects {
}
group = 'org.json'
-version = 'v20260522-SNAPSHOT'
+version = 'v20260719-SNAPSHOT'
description = 'JSON in Java'
sourceCompatibility = '1.8'
diff --git a/docs/RELEASES.md b/docs/RELEASES.md
index 7513766ff..43edf3ae5 100644
--- a/docs/RELEASES.md
+++ b/docs/RELEASES.md
@@ -5,6 +5,8 @@ and artifactId "json". For example:
[https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav](https://search.maven.org/search?q=g:org.json%20AND%20a:json&core=gav)
~~~
+20260719 Fixes CVE-2026-59171 very large BigInteger, BigDecimal
+
20260522 Publish key data, recent commits for minor fixes
20251224 Records, fromJson(), and recent commits
diff --git a/pom.xml b/pom.xml
index 3f15d6896..d5fb42ddc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
org.json
json
- 20260522
+ 20260719
bundle
JSON in Java
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 2471aa037..bcd218e5d 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,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)) {
@@ -1411,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 {
/**
@@ -1422,7 +1507,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) {
@@ -2667,16 +2757,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 +2807,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() == ParserConfiguration.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..e80093cac 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,32 @@ 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 Use the with*() methods instead
*/
+ @Deprecated
protected ParserConfiguration(final boolean keepStrings, final int maxNestingDepth) {
this.keepStrings = keepStrings;
this.maxNestingDepth = maxNestingDepth;
+ this.maxNumberLength = DEFAULT_MAX_NUMBER_LENGTH;
}
/**
@@ -58,10 +77,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 +143,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..f5846cc1e 100644
--- a/src/main/java/org/json/XML.java
+++ b/src/main/java/org/json/XML.java
@@ -172,8 +172,14 @@ static boolean mustEscape(int cp) {
&& cp != 0xA
&& cp != 0xD
) || !(
- // valid the range of acceptable characters that aren't control
- (cp >= 0x20 && cp <= 0xD7FF)
+ // Valid character range per W3C XML 1.0 spec:
+ // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ // Previously omitted #x9/#xA/#xD, causing unescape("
") etc.
+ // to reject valid LF/TAB/CR as illegal (see #1059)
+ cp == 0x9
+ || cp == 0xA
+ || cp == 0xD
+ || (cp >= 0x20 && cp <= 0xD7FF)
|| (cp >= 0xE000 && cp <= 0xFFFD)
|| (cp >= 0x10000 && cp <= 0x10FFFF)
)
@@ -617,16 +623,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 +672,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() == ParserConfiguration.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..6b692789e 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;
@@ -1368,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
@@ -3796,38 +3885,96 @@ public void jsonObjectParseFromJson_9() {
@Test
public void testMaxNumberLength() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; ++i) {
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);
+
+ // numbers without quotes are not allowed in strict mode
+ if (!jsonParserConfiguration.isStrictMode()) {
+ // 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));
+
+ // numbers without quotes are not allowed in strict mode
+ if (!jsonParserConfiguration.isStrictMode()) {
+ // 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());
}
/**
@@ -3835,6 +3982,8 @@ public void testMaxNumberLength() {
*/
@Test
public void testMaxNumberLengthNegativeInteger() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+
// Build a negative number string of exactly 1000 chars: "-" + 999 digits
StringBuilder sb = new StringBuilder("-");
for (int i = 0; i < 999; ++i) {
@@ -3843,16 +3992,15 @@ 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);
+ // numbers without quotes are not allowed in strict mode
+ if (!jsonParserConfiguration.isStrictMode()) {
+ // over max length: returned as string
+ sb.append("1");
+ assertEquals(1001, sb.length());
+ checkJSONObjectMaxLen(sb.toString(), false, null);
+ }
}
/**
@@ -3860,6 +4008,8 @@ public void testMaxNumberLengthNegativeInteger() {
*/
@Test
public void testMaxNumberLengthDecimal() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+
// Build a decimal number string of exactly 1000 chars: 499 digits + "." + 500
// digits
StringBuilder sb = new StringBuilder();
@@ -3873,16 +4023,15 @@ 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);
-
- // 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(), true, null);
+
+ // numbers without quotes are not allowed in strict mode
+ if (!jsonParserConfiguration.isStrictMode()) {
+ // over max length: returned as string
+ sb.append("3");
+ assertEquals(1001, sb.length());
+ checkJSONObjectMaxLen(sb.toString(), false, null);
+ }
}
/**
@@ -3890,6 +4039,8 @@ public void testMaxNumberLengthDecimal() {
*/
@Test
public void testMaxNumberLengthScientificNotation() {
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+
// Build a scientific notation string of exactly 1000 chars
StringBuilder sb = new StringBuilder("1.");
for (int i = 0; i < 994; ++i) {
@@ -3899,20 +4050,20 @@ 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.");
for (int i = 0; i < 995; ++i) {
sb.append("0");
}
- 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);
+
+ // numbers without quotes are not allowed in strict mode
+ if (!jsonParserConfiguration.isStrictMode()) {
+ sb.append("e100");
+ assertEquals(1001, sb.length());
+ checkJSONObjectMaxLen(sb.toString(), false, null);
+ }
}
/**
@@ -4169,17 +4320,21 @@ public void testStringToValueViaJSONObject() {
*/
@Test
public void testStringToNumberInvalidFormats() {
- // Leading zero followed by digit → treated as string (not octal)
- JSONObject jo = new JSONObject("{\"a\": 01}");
- // JSONTokener with strict mode would reject, but default mode stores as string
- // since stringToNumber throws NumberFormatException for "01"
- Object val = jo.get("a");
- assertTrue("01 should be stored as string, got " + val.getClass(), val instanceof String);
-
- // Negative with leading zero → "-01"
- jo = new JSONObject("{\"a\": -01}");
- val = jo.get("a");
- assertTrue("-01 should be stored as string, got " + val.getClass(), val instanceof String);
+ // this test should only run in non-strict mode, otherwise it will throw an exception
+ JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration();
+ if (!jsonParserConfiguration.isStrictMode()) {
+ // Leading zero followed by digit → treated as string (not octal)
+ JSONObject jo = new JSONObject("{\"a\": 01}");
+ // JSONTokener with strict mode would reject, but default mode stores as string
+ // since stringToNumber throws NumberFormatException for "01"
+ Object val = jo.get("a");
+ assertTrue("01 should be stored as string, got " + val.getClass(), val instanceof String);
+
+ // Negative with leading zero → "-01"
+ jo = new JSONObject("{\"a\": -01}");
+ val = jo.get("a");
+ assertTrue("-01 should be stored as string, got " + val.getClass(), val instanceof String);
+ }
}
}
diff --git a/src/test/java/org/json/junit/XMLTest.java b/src/test/java/org/json/junit/XMLTest.java
index 589536fd2..44fdcccb4 100644
--- a/src/test/java/org/json/junit/XMLTest.java
+++ b/src/test/java/org/json/junit/XMLTest.java
@@ -1537,6 +1537,43 @@ public void testValidUppercaseHexEntity() {
assertEquals("A", jsonObject.getString("a"));
}
+ /**
+ * Tests that valid XML numeric character references for whitespace
+ * control characters (TAB, LF, CR) are correctly unescaped. These
+ * codepoints are explicitly allowed by the XML 1.0 spec
+ * (https://www.w3.org/TR/REC-xml/#charsets) but were previously rejected
+ * as invalid. See issue #1059.
+ */
+ @Test
+ public void testValidWhitespaceNumericEntityUnescape() {
+ // decimal references for the three allowed control characters
+ assertEquals("\t", XML.unescape(" "));
+ assertEquals("\n", XML.unescape("
"));
+ assertEquals("\r", XML.unescape("
"));
+ // hex references for the same codepoints
+ assertEquals("\t", XML.unescape(" "));
+ assertEquals("\n", XML.unescape("
"));
+ assertEquals("\r", XML.unescape("
"));
+ }
+
+ /**
+ * Tests that {@code XML.toJSONObject} accepts numeric character references
+ * for the XML-allowed control characters (TAB, LF, CR) without throwing.
+ * Regression test for #1059, where {@code XML.toJSONObject("
")}
+ * threw JSONException in versions after 20251224.
+ */
+ @Test
+ public void testValidWhitespaceNumericEntityToJSONObject() {
+ // LF reference should round-trip through toJSONObject without throwing
+ JSONObject jsonObject = XML.toJSONObject("
");
+ // the value is the LF character (possibly trimmed by the JSON path,
+ // but the call must not throw)
+ assertTrue(jsonObject.has("a"));
+ // TAB and CR references also accepted
+ XML.toJSONObject(" ");
+ XML.toJSONObject("
");
+ }
+
}