This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c23b74e287c5f70d9f1fd0ca691f7ad1444e870e Author: opencode <[email protected]> AuthorDate: Thu Oct 15 09:06:02 2026 +0200 Add a maximum nesting depth to the JSON parser The parser recursed once per nesting level with no limit, so deeply nested input caused a StackOverflowError. That Error also escaped the consumers of the parser, which typically only catch Exception. The parser now counts nesting at the entry point of the anything() production and throws a ParseException once the depth exceeds 1000. RFC 8259 allows implementations to set a limit on the maximum depth of nesting. A depth of 1000 is far beyond what realistic JSON documents use and matches the limit used by other mainstream JSON parsers. The count is reset by the public entry points parseObject(), parseArray() and parse() so that a parser remains usable after a failed parse. The change is made in the grammar (JSONParser.jjt) and mirrored in the checked-in generated parser. Regression tests cover acceptance at the limit, rejection above it, and reuse of a parser after a depth error. --- java/org/apache/tomcat/util/json/JSONParser.java | 17 ++++++++++ java/org/apache/tomcat/util/json/JSONParser.jjt | 23 ++++++++++++- .../apache/tomcat/util/json/TestJSONParser.java | 38 ++++++++++++++++++++++ webapps/docs/changelog.xml | 8 +++++ 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/java/org/apache/tomcat/util/json/JSONParser.java b/java/org/apache/tomcat/util/json/JSONParser.java index 4a2e3d8bd4..b798831c1e 100644 --- a/java/org/apache/tomcat/util/json/JSONParser.java +++ b/java/org/apache/tomcat/util/json/JSONParser.java @@ -30,6 +30,16 @@ public class JSONParser implements JSONParserConstants { private boolean nativeNumbers = false; + /* + * The maximum nesting depth accepted by the parser. Deeply nested input would otherwise make the recursive + * descent exhaust the stack of the thread running the parser. RFC 8259 allows implementations to set a limit + * on the maximum depth of nesting. A depth of 1000 is far beyond what realistic JSON documents use and + * matches the limit used by other mainstream JSON parsers. + */ + private static final int MAX_DEPTH = 1000; + + private int depth = 0; + public JSONParser(String input) { this(new java.io.StringReader(input)); } @@ -38,6 +48,7 @@ public class JSONParser implements JSONParserConstants { * Parses a JSON object into a Java {@code Map}. */ public java.util.LinkedHashMap<String,Object> parseObject() throws ParseException { + depth = 0; java.util.LinkedHashMap<String,Object> toReturn = object(); if (!ensureEOF()) { throw new IllegalStateException("Expected EOF, but still had content to parse"); @@ -49,6 +60,7 @@ public class JSONParser implements JSONParserConstants { * Parses a JSON array into a Java {@code List}. */ public java.util.ArrayList<Object> parseArray() throws ParseException { + depth = 0; java.util.ArrayList<Object> toReturn = list(); if (!ensureEOF()) { throw new IllegalStateException("Expected EOF, but still had content to parse"); @@ -60,6 +72,7 @@ public class JSONParser implements JSONParserConstants { * Parses any JSON-parsable object, returning the value. */ public Object parse() throws ParseException { + depth = 0; Object toReturn = anything(); if (!ensureEOF()) { throw new IllegalStateException("Expected EOF, but still had content to parse"); @@ -149,6 +162,9 @@ public class JSONParser implements JSONParserConstants { final public Object anything() throws ParseException { Object x; + if (++depth > MAX_DEPTH) { + throw new ParseException("Maximum nesting depth " + MAX_DEPTH + " exceeded"); + } switch (jj_nt.kind) { case BRACE_OPEN: { x = object(); @@ -177,6 +193,7 @@ public class JSONParser implements JSONParserConstants { } { if ("" != null) { + depth--; return x; } } diff --git a/java/org/apache/tomcat/util/json/JSONParser.jjt b/java/org/apache/tomcat/util/json/JSONParser.jjt index e794dfab60..7504ef110f 100644 --- a/java/org/apache/tomcat/util/json/JSONParser.jjt +++ b/java/org/apache/tomcat/util/json/JSONParser.jjt @@ -61,6 +61,16 @@ public class JSONParser { private boolean nativeNumbers = false; + /* + * The maximum nesting depth accepted by the parser. Deeply nested input would otherwise make the recursive + * descent exhaust the stack of the thread running the parser. RFC 8259 allows implementations to set a limit + * on the maximum depth of nesting. A depth of 1000 is far beyond what realistic JSON documents use and + * matches the limit used by other mainstream JSON parsers. + */ + private static final int MAX_DEPTH = 1000; + + private int depth = 0; + public JSONParser(String input) { this(new java.io.StringReader(input)); } @@ -69,6 +79,7 @@ public class JSONParser { * Parses a JSON object into a Java {@code Map}. */ public java.util.LinkedHashMap<String, Object> parseObject() throws ParseException { + depth = 0; java.util.LinkedHashMap<String, Object> toReturn = object(); if (!ensureEOF()) { throw new IllegalStateException("Expected EOF, but still had content to parse"); @@ -80,6 +91,7 @@ public class JSONParser { * Parses a JSON array into a Java {@code List}. */ public java.util.ArrayList<Object> parseArray() throws ParseException { + depth = 0; java.util.ArrayList<Object> toReturn = list(); if (!ensureEOF()) { throw new IllegalStateException("Expected EOF, but still had content to parse"); @@ -91,6 +103,7 @@ public class JSONParser { * Parses any JSON-parsable object, returning the value. */ public Object parse() throws ParseException { + depth = 0; Object toReturn = anything(); if (!ensureEOF()) { throw new IllegalStateException("Expected EOF, but still had content to parse"); @@ -270,11 +283,19 @@ boolean ensureEOF() : {}{ Object anything() : { Object x; }{ + { + if (++depth > MAX_DEPTH) { + throw new ParseException("Maximum nesting depth " + MAX_DEPTH + " exceeded"); + } + } ( x = object() | x = list() | x = value() ) - { return x; } + { + depth--; + return x; + } } String objectKey() : { diff --git a/test/org/apache/tomcat/util/json/TestJSONParser.java b/test/org/apache/tomcat/util/json/TestJSONParser.java index 33a6c77aeb..bd8f7abfb3 100644 --- a/test/org/apache/tomcat/util/json/TestJSONParser.java +++ b/test/org/apache/tomcat/util/json/TestJSONParser.java @@ -16,15 +16,53 @@ */ package org.apache.tomcat.util.json; +import java.io.StringReader; + import org.junit.Assert; import org.junit.Test; public class TestJSONParser { + private static final int MAX_TEST_DEPTH = 1000; + @Test public void testUnicodeEscapeIsNotUnescapedTwice() throws ParseException { String json = "\"\\u005cn\""; Assert.assertEquals("\\n", new JSONParser(json).parse()); } + + @Test + public void testNestingAtMaximumDepthAccepted() throws ParseException { + Assert.assertNotNull(new JSONParser(nest(MAX_TEST_DEPTH)).parse()); + } + + @Test(expected = ParseException.class) + public void testNestingAboveMaximumDepthIsParseException() throws ParseException { + new JSONParser(nest(MAX_TEST_DEPTH + 1)).parse(); + } + + @Test + public void testDepthResetAfterError() throws ParseException { + JSONParser parser = new JSONParser(nest(MAX_TEST_DEPTH + 1)); + try { + parser.parse(); + Assert.fail("A ParseException was expected"); + } catch (ParseException e) { + // Expected + } + parser.ReInit(new StringReader("[1]")); + Assert.assertNotNull(parser.parse()); + } + + private static String nest(int depth) { + StringBuilder result = new StringBuilder(depth * 2); + for (int i = 0; i < depth; i++) { + result.append('['); + } + for (int i = 0; i < depth; i++) { + result.append(']'); + } + return result.toString(); + } } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 5ebf8c88a6..9c54eb6352 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -331,6 +331,14 @@ corrected to terminate, since the Java shift distance is evaluated modulo 32. (remm) </fix> + <fix> + The JSON parser now rejects input with a nesting depth greater than + 1000 with a <code>ParseException</code>. Previously, deeply nested + input caused a <code>StackOverflowError</code> since the parser + recursed once per nesting level with no limit. RFC 8259 allows + implementations to set a limit on the maximum depth of nesting. + (remm) + </fix> </changelog> </subsection> <subsection name="Web applications"> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
