This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5666-input-length-limits in repository https://gitbox.apache.org/repos/asf/struts.git
commit 33fe16bb0fa86d77567842ab3dbf3856dfc50b73 Author: Lukasz Lenart <[email protected]> AuthorDate: Wed Jul 29 07:45:12 2026 +0200 WW-5666 fix(json): apply the input length limit while reading The configured JSON input length limit was evaluated after accumulating each line of input. It is now evaluated as the input is read, in fixed-size chunks, so enforcement no longer varies with the structure of the input. Line terminators are no longer stripped while reading. They are insignificant whitespace between tokens, but an unescaped control character inside a string value is now preserved rather than silently removed. --- .../java/org/apache/struts2/json/JSONUtil.java | 13 ++- .../struts2/json/JSONUtilInputLimitTest.java | 113 +++++++++++++++++++++ 2 files changed, 122 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 78bceaacf..e1c20f7b1 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 @@ -59,6 +59,9 @@ public class JSONUtil { private static final Logger LOG = LogManager.getLogger(JSONUtil.class); + /** Chunk size used to read input incrementally while applying the length limit. */ + private static final int READ_CHUNK_SIZE = 8192; + private JSONReader reader; private JSONWriter writer; @@ -297,13 +300,15 @@ public class JSONUtil { * @throws JSONException when IOException happens or limits are exceeded */ public Object deserializeInput(Reader reader, int maxLength) throws JSONException { - BufferedReader bufferReader = new BufferedReader(reader); - String line; StringBuilder buffer = new StringBuilder(); + 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 exceeds maximum allowed length (" + maxLength + "). Use " + JSONConstants.JSON_MAX_LENGTH + " to increase the limit."); 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..ce6edaddb --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilInputLimitTest.java @@ -0,0 +1,113 @@ +/* + * 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.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * Verifies that {@link JSONUtil#deserializeInput(Reader, 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() { + } + } + + @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); + + assertThrows(JSONException.class, () -> util.deserializeInput(input, maxLength)); + + 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(); + util.setReader(new StrutsJSONReader()); + + Object result = util.deserializeInput(new StringReader("{\"a\":1, \"b\":\"hello\"}"), 1024); + + 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(); + util.setReader(new StrutsJSONReader()); + + // Line terminators between tokens are insignificant whitespace to the reader. + Object result = util.deserializeInput(new StringReader("{\n\"a\":1,\n\"b\":2\n}"), 1024); + + assertTrue("Expected a parsed JSON object", result instanceof Map); + assertEquals(1L, ((Map<?, ?>) result).get("a")); + assertEquals(2L, ((Map<?, ?>) result).get("b")); + } +}
