This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5666-input-length-limits-6x in repository https://gitbox.apache.org/repos/asf/struts.git
commit 828d9a984ecca996e668a6b4646d04fa0d6079d2 Author: Lukasz Lenart <[email protected]> AuthorDate: Fri Jul 31 11:09:45 2026 +0200 WW-5666 fix(json): apply the input length limit while reading --- .../java/org/apache/struts2/json/JSONUtil.java | 13 ++- .../struts2/json/JSONUtilInputLimitTest.java | 119 +++++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java index 10f063ec8..2fcb549b3 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java @@ -57,6 +57,9 @@ public class JSONUtil { public final static String RFC3339_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; public static final boolean CACHE_BEAN_INFO_DEFAULT = true; + + /** Chunk size used to read input incrementally while applying the length limit. */ + private static final int READ_CHUNK_SIZE = 8192; private static final Logger LOG = LogManager.getLogger(JSONUtil.class); @@ -337,13 +340,15 @@ public class JSONUtil { */ public Object deserializeInput(Reader reader, int maxLength, int maxElements, int maxDepth, int maxStringLength, int maxKeyLength) throws JSONException { - BufferedReader bufferReader = new BufferedReader(reader); StringBuilder buffer = new StringBuilder(); - String line; + char[] chunk = new char[READ_CHUNK_SIZE]; try { - while ((line = bufferReader.readLine()) != null) { - buffer.append(line); + int read; + // Apply the limit while reading rather than afterwards, so input that contains no + // line terminator is not accumulated in full before the limit can be evaluated. + while ((read = reader.read(chunk)) != -1) { + buffer.append(chunk, 0, read); if (buffer.length() > maxLength) { throw new JSONException("JSON input length exceeds maximum allowed length of " + maxLength); } diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilInputLimitTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilInputLimitTest.java new file mode 100644 index 000000000..698a33030 --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilInputLimitTest.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.json; + +import org.junit.Test; + +import java.io.Reader; +import java.io.StringReader; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Verifies that {@link JSONUtil#deserializeInput(Reader, int, int, int, int, int)} applies the + * configured input length limit while reading, bounding how much input is consumed before the limit + * takes effect, and that input within the limit still parses. + */ +public class JSONUtilInputLimitTest { + + /** + * Emits {@code total} characters with no line terminator anywhere, and records how many + * characters the caller actually consumed. + */ + private static final class UnterminatedReader extends Reader { + private final long total; + private final AtomicLong consumed; + private long produced = 0; + + UnterminatedReader(long total, AtomicLong consumed) { + this.total = total; + this.consumed = consumed; + } + + @Override + public int read(char[] cbuf, int off, int len) { + if (produced >= total) { + return -1; + } + int count = (int) Math.min(len, total - produced); + for (int i = 0; i < count; i++) { + cbuf[off + i] = 'a'; + } + produced += count; + consumed.addAndGet(count); + return count; + } + + @Override + public void close() { + // characters are generated on demand, so there is nothing to release + } + } + + @Test + public void inputWithoutLineTerminatorIsLimitedWhileReading() { + int maxLength = 1024; + long inputSize = 64L * 1024 * 1024; + AtomicLong consumed = new AtomicLong(); + + JSONUtil util = new JSONUtil(); + Reader input = new UnterminatedReader(inputSize, consumed); + + try { + util.deserializeInput(input, maxLength, 100, 10, 1000, 100); + fail("Expected JSONException for exceeding max length"); + } catch (JSONException expected) { + // the limit is expected to be reported + } + + long read = consumed.get(); + // Reading proceeds in chunks, so a single chunk of overshoot beyond the limit is expected. + assertTrue("Consumed " + read + " characters for a limit of " + maxLength, + read < maxLength + 65_536L); + } + + @Test + public void inputWithinLimitIsParsed() throws JSONException { + JSONUtil util = new JSONUtil(); + + Object result = util.deserializeInput( + new StringReader("{\"a\":1, \"b\":\"hello\"}"), 1024, 100, 10, 1000, 100); + + assertTrue("Expected a parsed JSON object", result instanceof Map); + assertEquals(1L, ((Map<?, ?>) result).get("a")); + assertEquals("hello", ((Map<?, ?>) result).get("b")); + } + + @Test + public void inputSpanningMultipleLinesIsParsed() throws JSONException { + JSONUtil util = new JSONUtil(); + + // Line terminators between tokens are insignificant whitespace to the reader. + Object result = util.deserializeInput( + new StringReader("{\n\"a\":1,\n\"b\":2\n}"), 1024, 100, 10, 1000, 100); + + assertTrue("Expected a parsed JSON object", result instanceof Map); + assertEquals(1L, ((Map<?, ?>) result).get("a")); + assertEquals(2L, ((Map<?, ?>) result).get("b")); + } +}
