This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 13a8b44f8309a1d76429477e17819ba0b7df4a61 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 664c31e0a1..674707dfff 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -527,6 +527,14 @@ closes the entire connection. These frames are already permitted when the stream is closed in the other direction. (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="Jasper"> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
