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"));
+    }
+}

Reply via email to