You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by re...@apache.org on 2026/06/25 14:08:23 UTC

(cxf) branch 3.6.x-fixes updated (8a75a4fcd81 -> 35d62a6d4a3)

This is an automated email from the ASF dual-hosted git repository.

reta pushed a change to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


    from 8a75a4fcd81 Other checkstyle fix
     new a2f43e5c475 Add limit to the maximum number of attachment headers to be collected (#3159)
     new d061d648cda Introduce default value for maxFormParameterCount (#3177)
     new 35d62a6d4a3 Add sensible default value to attachment-max-size property (#3188)

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../cxf/attachment/AttachmentDeserializer.java     | 118 ++------------
 .../cxf/attachment/AttachmentDeserializerUtil.java | 181 +++++++++++++++++++++
 .../org/apache/cxf/attachment/AttachmentUtil.java  |  33 ++--
 .../cxf/attachment/AttachmentDeserializerTest.java |  45 +++++
 .../java/org/apache/cxf/jaxrs/utils/FormUtils.java |   9 +-
 .../jaxrs/provider/FormEncodingProviderTest.java   |  49 ++++++
 .../org/apache/cxf/jaxrs/utils/FormUtilsTest.java  | 103 +++++++++---
 7 files changed, 397 insertions(+), 141 deletions(-)
 create mode 100644 core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializerUtil.java


(cxf) 02/03: Introduce default value for maxFormParameterCount (#3177)

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit d061d648cda9e7fa000a861bab1d20ca2ece8717
Author: Andriy Redko <dr...@gmail.com>
AuthorDate: Thu Jun 4 11:40:57 2026 -0400

    Introduce default value for maxFormParameterCount (#3177)
---
 .../java/org/apache/cxf/jaxrs/utils/FormUtils.java |   9 +-
 .../org/apache/cxf/jaxrs/utils/FormUtilsTest.java  | 103 +++++++++++++++++----
 2 files changed, 88 insertions(+), 24 deletions(-)

diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java
index 0c14eb0b29c..5d51005b154 100644
--- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/FormUtils.java
@@ -54,6 +54,8 @@ import org.apache.cxf.phase.PhaseInterceptorChain;
 import org.apache.cxf.transport.http.AbstractHTTPDestination;
 
 public final class FormUtils {
+    public static final int DEFAULT_MAX_FORM_PARAM_COUNT = 500;
+
     public static final String FORM_PARAMS_FROM_HTTP_PARAMS = "set.form.parameters.from.http.parameters";
     public static final String FORM_PARAM_MAP = "org.apache.cxf.form_data";
     public static final String FORM_PARAM_MAP_DECODED = "org.apache.cxf.form_data.decoded";
@@ -272,11 +274,8 @@ public final class FormUtils {
         if (m == null || m.getExchange() == null || m.getExchange().getInMessage() == null) {
             return;
         }
-        String maxPartsCountProp = (String)m.getExchange()
-            .getInMessage().getContextualProperty(MAX_FORM_PARAM_COUNT);
-        if (maxPartsCountProp == null) {
-            return;
-        }
+        final String maxPartsCountProp = MessageUtils.getContextualString(m.getExchange().getInMessage(),
+            MAX_FORM_PARAM_COUNT, Integer.toString(DEFAULT_MAX_FORM_PARAM_COUNT));
         try {
             int maxPartsCount = Integer.parseInt(maxPartsCountProp);
             if (maxPartsCount != -1 && numberOfParts >= maxPartsCount) {
diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/FormUtilsTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/FormUtilsTest.java
index 4e55527ea05..3076a291d4f 100644
--- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/FormUtilsTest.java
+++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/FormUtilsTest.java
@@ -19,41 +19,55 @@
 
 package org.apache.cxf.jaxrs.utils;
 
+import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MultivaluedMap;
 
+import org.apache.cxf.jaxrs.ext.multipart.Attachment;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
 import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.message.ExchangeImpl;
 import org.apache.cxf.message.Message;
 
 import org.junit.Test;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 public class FormUtilsTest {
-
-    private static final String HTTP_PARAM1 = "httpParam1";
-    private static final String HTTP_PARAM2 = "httpParam2";
-    private static final String HTTP_PARAM_VALUE1 = "httpValue1";
-    private static final String HTTP_PARAM_VALUE2 = "httpValue2";
-
-    private static final String FORM_PARAM1 = "formParam1";
-    private static final String FORM_PARAM2 = "formParam2";
-    private static final String FORM_PARAM_VALUE1 = "formValue1";
-    private static final String FORM_PARAM_VALUE2 = "formValue2";
+    private static final String HTTP_PARAM = "httpParam";
+    private static final String HTTP_PARAM1 = HTTP_PARAM + "1";
+    private static final String HTTP_PARAM2 = HTTP_PARAM + "2";
+    private static final String HTTP_PARAM_VALUE = "httpValue";
+    private static final String HTTP_PARAM_VALUE1 = HTTP_PARAM_VALUE + "1";
+    private static final String HTTP_PARAM_VALUE2 = HTTP_PARAM_VALUE + "2";
+
+    private static final String FORM_PARAM = "formParam";
+    private static final String FORM_PARAM1 = FORM_PARAM + "1";
+    private static final String FORM_PARAM2 = FORM_PARAM + "2";
+    private static final String FORM_PARAM_VALUE = "formValue";
+    private static final String FORM_PARAM_VALUE1 = FORM_PARAM_VALUE + "1";
+    private static final String FORM_PARAM_VALUE2 = FORM_PARAM_VALUE + "2";
 
     private Message mockMessage;
     private HttpServletRequest mockRequest;
 
     @Test
     public void populateMapFromStringFromHTTP() {
-        mockObjects(null);
+        mockObjects(null, 2);
 
         MultivaluedMap<String, String> params = new MetadataMap<>();
         FormUtils.populateMapFromString(params, mockMessage, null, StandardCharsets.UTF_8.name(),
@@ -66,7 +80,7 @@ public class FormUtilsTest {
 
     @Test
     public void populateMapFromStringFromHTTPWithProp() {
-        mockObjects("false");
+        mockObjects("false", 2);
 
         MultivaluedMap<String, String> params = new MetadataMap<>();
         FormUtils.populateMapFromString(params, mockMessage, null, StandardCharsets.UTF_8.name(),
@@ -77,7 +91,7 @@ public class FormUtilsTest {
 
     @Test
     public void populateMapFromStringFromBody() {
-        mockObjects(null);
+        mockObjects(null, 2);
 
         MultivaluedMap<String, String> params = new MetadataMap<>();
         String postBody = FORM_PARAM1 + "=" + FORM_PARAM_VALUE1 + "&" + FORM_PARAM2 + "=" + FORM_PARAM_VALUE2;
@@ -90,19 +104,70 @@ public class FormUtilsTest {
     }
 
 
-    private void mockObjects(String formPropertyValue) {
+    @Test
+    public void populateMapFromBodyExceedsDefaultMaxFormParams() {
+        mockObjects(null, FormUtils.DEFAULT_MAX_FORM_PARAM_COUNT);
+        
+        final MultivaluedMap<String, String> params = new MetadataMap<>();
+        final String postBody = IntStream
+                .range(1, FormUtils.DEFAULT_MAX_FORM_PARAM_COUNT + 1)
+                .mapToObj(i -> FORM_PARAM + i + "=" + FORM_PARAM_VALUE + i)
+                .collect(Collectors.joining("&"));
+        final WebApplicationException ex = assertThrows(WebApplicationException.class,
+            () -> FormUtils.populateMapFromString(params, mockMessage, postBody, 
+                StandardCharsets.UTF_8.name(), false));
+        assertThat(ex.getResponse().getStatus(), equalTo(413)); /* Request Entity Too Large */
+
+        // Increase the limit and try again
+        when(mockMessage.getContextualProperty("maxFormParameterCount"))
+            .thenReturn(Integer.toString(FormUtils.DEFAULT_MAX_FORM_PARAM_COUNT + 1));
+        FormUtils.populateMapFromString(params, mockMessage, null, StandardCharsets.UTF_8.name(), false, mockRequest);
+        assertEquals(500, params.size());
+    }
+    
+    @Test
+    public void populateMapFromMultiPartExceedsDefaultMaxFormParams() {
+        mockObjects(null, FormUtils.DEFAULT_MAX_FORM_PARAM_COUNT);
+
+        final MultivaluedMap<String, String> params = new MetadataMap<>();
+        final MultipartBody body = new MultipartBody(IntStream
+                .range(1, FormUtils.DEFAULT_MAX_FORM_PARAM_COUNT + 1)
+                .mapToObj(i -> {
+                    final MultivaluedMap<String, String> headers = new MetadataMap<>();
+                    headers.putSingle("Content-ID", UUID.randomUUID().toString());
+                    return new Attachment(InputStream.nullInputStream(), headers);
+                })
+                .collect(Collectors.toList()));
+        final WebApplicationException ex = assertThrows(WebApplicationException.class,
+            () -> FormUtils.populateMapFromMultipart(params, body, mockMessage, false));
+        assertThat(ex.getResponse().getStatus(), equalTo(413)); /* Request Entity Too Large */
+
+        // Increase the limit and try again
+        when(mockMessage.getContextualProperty("maxFormParameterCount"))
+            .thenReturn(Integer.toString(FormUtils.DEFAULT_MAX_FORM_PARAM_COUNT + 1));
+        FormUtils.populateMapFromMultipart(params, body, mockMessage, false);
+        assertEquals(500, params.size());
+    }
+
+    private void mockObjects(String formPropertyValue, int params) {
+        final ExchangeImpl exchange = new ExchangeImpl();
+
         mockMessage = mock(Message.class);
         when(mockMessage.getContextualProperty(FormUtils.FORM_PARAMS_FROM_HTTP_PARAMS))
             .thenReturn(formPropertyValue);
-        when(mockMessage.getExchange()).thenReturn(null);
+        when(mockMessage.getExchange()).thenReturn(exchange);
         when(mockMessage.put(FormUtils.FORM_PARAM_MAP_DECODED, true))
             .thenReturn(null);
-        
+        exchange.setInMessage(mockMessage);
+
         mockRequest = mock(HttpServletRequest.class);
-        String[] httpParamNames = {HTTP_PARAM1, HTTP_PARAM2};
+        String[] httpParamNames =  IntStream.range(1, params + 1)
+            .mapToObj(i -> "httpParam" + i)
+            .toArray(String[]::new);
         Enumeration<String> httpParamsEnum = Collections.enumeration(Arrays.asList(httpParamNames));
         when(mockRequest.getParameterNames()).thenReturn(httpParamsEnum);
-        when(mockRequest.getParameterValues(HTTP_PARAM1)).thenReturn(new String[] {HTTP_PARAM_VALUE1});
-        when(mockRequest.getParameterValues(HTTP_PARAM2)).thenReturn(new String[] {HTTP_PARAM_VALUE2});
+        for (int i = 1; i <= httpParamNames.length; ++i) {
+            when(mockRequest.getParameterValues(HTTP_PARAM + i)).thenReturn(new String[] {HTTP_PARAM_VALUE + i});
+        }
     }
 }
\ No newline at end of file


(cxf) 01/03: Add limit to the maximum number of attachment headers to be collected (#3159)

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit a2f43e5c475ff6f7dd5ab23388ae8e827d05a8db
Author: Andriy Redko <dr...@gmail.com>
AuthorDate: Tue Jun 2 11:36:41 2026 -0400

    Add limit to the maximum number of attachment headers to be collected (#3159)
---
 .../cxf/attachment/AttachmentDeserializer.java     | 116 ++-----------
 .../cxf/attachment/AttachmentDeserializerUtil.java | 181 +++++++++++++++++++++
 .../cxf/attachment/AttachmentDeserializerTest.java |  23 +++
 3 files changed, 219 insertions(+), 101 deletions(-)

diff --git a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
index 7827b8d5014..dfe76e6b4b1 100644
--- a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
+++ b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
@@ -30,14 +30,11 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.TreeMap;
-import java.util.logging.Logger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.activation.DataSource;
 
-import org.apache.cxf.common.logging.LogUtils;
 import org.apache.cxf.common.util.StringUtils;
 import org.apache.cxf.common.util.SystemPropertyAction;
 import org.apache.cxf.helpers.HttpHeaderHelper;
@@ -71,6 +68,13 @@ public class AttachmentDeserializer {
      */
     public static final String ATTACHMENT_MAX_COUNT = "attachment-max-count";
 
+    /**
+     * The maximum number of attachment headers permitted in a message. The default is 500.
+     */
+    public static final String ATTACHMENT_HEADERS_MAX_COUNT = "attachment-headers-max-count";
+    public static final int DEFAULT_ATTACHMENT_HEADERS_MAX_COUNT =
+        SystemPropertyAction.getInteger("org.apache.cxf.attachment-max-headers-count", 500);
+
     /**
      * The maximum MIME Header Length. The default is 300.
      */
@@ -85,8 +89,6 @@ public class AttachmentDeserializer {
     private static final Pattern INPUT_STREAM_BOUNDARY_PATTERN =
             Pattern.compile("^--(\\S*)$", Pattern.MULTILINE);
 
-    private static final Logger LOG = LogUtils.getL7dLogger(AttachmentDeserializer.class);
-
     private static final int PUSHBACK_AMOUNT = 2048;
 
     private boolean lazyLoading = true;
@@ -108,6 +110,7 @@ public class AttachmentDeserializer {
     private List<String> supportedTypes;
 
     private int maxHeaderLength = DEFAULT_MAX_HEADER_SIZE;
+    private int maxHeadersCount = DEFAULT_ATTACHMENT_HEADERS_MAX_COUNT;
 
     public AttachmentDeserializer(Message message) {
         this(message, Collections.singletonList("multipart/related"));
@@ -120,6 +123,9 @@ public class AttachmentDeserializer {
         // Get the maximum Header length from configuration
         maxHeaderLength = MessageUtils.getContextualInteger(message, ATTACHMENT_MAX_HEADER_SIZE,
                                                             DEFAULT_MAX_HEADER_SIZE);
+        // Get the maximum headers count
+        maxHeadersCount = MessageUtils.getContextualInteger(message, ATTACHMENT_HEADERS_MAX_COUNT,
+                                                            DEFAULT_ATTACHMENT_HEADERS_MAX_COUNT);
     }
 
     public void initializeAttachments() throws IOException {
@@ -166,7 +172,8 @@ public class AttachmentDeserializer {
                 throw new IOException("Couldn't find MIME boundary: " + boundaryString);
             }
 
-            Map<String, List<String>> ih = loadPartHeaders(stream);
+            final Map<String, List<String>> ih = AttachmentDeserializerUtil
+                .loadPartHeaders(stream, maxHeaderLength, maxHeadersCount);
             message.put(ATTACHMENT_PART_HEADERS, ih);
             String val = AttachmentUtil.getHeader(ih, "Content-Type", "; ");
             if (!StringUtils.isEmpty(val)) {
@@ -231,7 +238,8 @@ public class AttachmentDeserializer {
         }
         stream.unread(v);
 
-        Map<String, List<String>> headers = loadPartHeaders(stream);
+        final Map<String, List<String>> headers = AttachmentDeserializerUtil
+            .loadPartHeaders(stream, maxHeaderLength, maxHeadersCount);
         return (AttachmentImpl)createAttachment(headers);
     }
 
@@ -367,98 +375,4 @@ public class AttachmentDeserializer {
         stream.unread(v);
         return true;
     }
-
-
-
-    private Map<String, List<String>> loadPartHeaders(InputStream in) throws IOException {
-        StringBuilder buffer = new StringBuilder(128);
-        StringBuilder b = new StringBuilder(128);
-        Map<String, List<String>> heads = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-
-        // loop until we hit the end or a null line
-        while (readLine(in, b)) {
-            // lines beginning with white space get special handling
-            char c = b.charAt(0);
-            if (c == ' ' || c == '\t') {
-                if (buffer.length() != 0) {
-                    // preserve the line break and append the continuation
-                    buffer.append("\r\n");
-                    buffer.append(b);
-                }
-            } else {
-                // if we have a line pending in the buffer, flush it
-                if (buffer.length() > 0) {
-                    addHeaderLine(heads, buffer);
-                    buffer.setLength(0);
-                }
-                // add this to the accumulator
-                buffer.append(b);
-            }
-        }
-
-        // if we have a line pending in the buffer, flush it
-        if (buffer.length() > 0) {
-            addHeaderLine(heads, buffer);
-        }
-        return heads;
-    }
-
-    private boolean readLine(InputStream in, StringBuilder buffer) throws IOException {
-        if (buffer.length() != 0) {
-            buffer.setLength(0);
-        }
-        int c;
-
-        while ((c = in.read()) != -1) {
-            // a linefeed is a terminator, always.
-            if (c == '\n') {
-                break;
-            } else if (c == '\r') {
-                //just ignore the CR.  The next character SHOULD be an NL.  If not, we're
-                //just going to discard this
-                continue;
-            } else {
-                // just add to the buffer
-                buffer.append((char)c);
-            }
-
-            if (buffer.length() > maxHeaderLength) {
-                LOG.fine("The attachment header size has exceeded the configured parameter: " + maxHeaderLength);
-                throw new HeaderSizeExceededException();
-            }
-        }
-
-        // no characters found...this was either an eof or a null line.
-        return buffer.length() != 0;
-    }
-
-    private void addHeaderLine(Map<String, List<String>> heads, StringBuilder line) {
-        // null lines are a nop
-        final int size = line.length();
-        if (size == 0) {
-            return;
-        }
-        int separator = line.indexOf(":");
-        final String name;
-        String value = "";
-        if (separator == -1) {
-            name = line.toString().trim();
-        } else {
-            name = line.substring(0, separator);
-            // step past the separator.  Now we need to remove any leading white space characters.
-            separator++;
-
-            while (separator < size) {
-                char ch = line.charAt(separator);
-                if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
-                    break;
-                }
-                separator++;
-            }
-            value = line.substring(separator);
-        }
-        List<String> v = heads.computeIfAbsent(name, k -> new ArrayList<>(1));
-        v.add(value);
-    }
-
 }
diff --git a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializerUtil.java b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializerUtil.java
new file mode 100644
index 00000000000..c74d70c2962
--- /dev/null
+++ b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializerUtil.java
@@ -0,0 +1,181 @@
+/**
+ * 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.cxf.attachment;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.logging.Logger;
+
+import org.apache.cxf.common.logging.LogUtils;
+
+final class AttachmentDeserializerUtil {
+    /* Keep the log under AttachmentDeserializer */
+    private static final Logger LOG = LogUtils.getL7dLogger(AttachmentDeserializer.class);
+
+    private AttachmentDeserializerUtil() {
+    }
+    
+    /**
+     * Move the read pointer to the begining of the first part read till the end
+     * of first boundary
+     *
+     * @param pushbackInStream
+     * @param boundary
+     * @throws IOException
+     */
+    static boolean readTillFirstBoundary(PushbackInputStream pushbackInStream,
+        byte[] boundary) throws IOException {
+
+        // work around a bug in PushBackInputStream where the buffer isn't
+        // initialized
+        // and available always returns 0.
+        int value = pushbackInStream.read();
+        pushbackInStream.unread(value);
+        while (value != -1) {
+            value = pushbackInStream.read();
+            if ((byte) value == boundary[0]) {
+                int boundaryIndex = 0;
+                while (value != -1 
+                    && boundaryIndex < boundary.length 
+                    && (byte)value == boundary[boundaryIndex]) {
+
+                    value = pushbackInStream.read();
+                    if (value == -1) {
+                        throw new IOException("Unexpected End while searching for first Mime Boundary");
+                    }
+                    boundaryIndex++;
+                }
+                if (boundaryIndex == boundary.length) {
+                    // boundary found, read the newline
+                    if (value == 13) {
+                        pushbackInStream.read();
+                    }
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+
+    static Map<String, List<String>> loadPartHeaders(InputStream in, int maxHeaderLength, 
+            int maxHeadersCount) throws IOException {
+        StringBuilder buffer = new StringBuilder(128);
+        StringBuilder b = new StringBuilder(128);
+        Map<String, List<String>> heads = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+        // loop until we hit the end or a null line
+        while (readLine(in, b, maxHeaderLength)) {
+            // lines beginning with white space get special handling
+            char c = b.charAt(0);
+            if (c == ' ' || c == '\t') {
+                if (buffer.length() != 0) {
+                    // preserve the line break and append the continuation
+                    buffer.append("\r\n");
+                    buffer.append(b);
+                }
+            } else {
+                // if we have a line pending in the buffer, flush it
+                if (buffer.length() > 0) {
+                    addHeaderLine(heads, buffer, maxHeadersCount);
+                    buffer.setLength(0);
+                }
+                // add this to the accumulator
+                buffer.append(b);
+            }
+        }
+
+        // if we have a line pending in the buffer, flush it
+        if (buffer.length() > 0) {
+            addHeaderLine(heads, buffer, maxHeadersCount);
+        }
+        return heads;
+    }
+
+    private static boolean readLine(InputStream in, StringBuilder buffer, int maxHeaderLength) throws IOException {
+        if (buffer.length() != 0) {
+            buffer.setLength(0);
+        }
+        int c;
+
+        while ((c = in.read()) != -1) {
+            // a linefeed is a terminator, always.
+            if (c == '\n') {
+                break;
+            } else if (c == '\r') {
+                //just ignore the CR.  The next character SHOULD be an NL.  If not, we're
+                //just going to discard this
+                continue;
+            } else {
+                // just add to the buffer
+                buffer.append((char)c);
+            }
+
+            if (buffer.length() > maxHeaderLength) {
+                LOG.fine("The attachment header size has exceeded the configured parameter: " + maxHeaderLength);
+                throw new HeaderSizeExceededException();
+            }
+        }
+
+        // no characters found...this was either an eof or a null line.
+        return buffer.length() != 0;
+    }
+
+    private static void addHeaderLine(Map<String, List<String>> heads, StringBuilder line, 
+            int maxHeadersCount) throws IOException {
+        // null lines are a nop
+        final int size = line.length();
+        if (size == 0) {
+            return;
+        }
+        int separator = line.indexOf(":");
+        final String name;
+        String value = "";
+        if (separator == -1) {
+            name = line.toString().trim();
+        } else {
+            name = line.substring(0, separator);
+            // step past the separator.  Now we need to remove any leading white space characters.
+            separator++;
+
+            while (separator < size) {
+                char ch = line.charAt(separator);
+                if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
+                    break;
+                }
+                separator++;
+            }
+            value = line.substring(separator);
+        }
+        
+        if (heads.size() >= maxHeadersCount) {
+            throw new IOException("The attachment contains more headers than are permitted");
+        }
+        List<String> v = heads.computeIfAbsent(name, k -> new ArrayList<>(1));
+        v.add(value);
+    }
+
+
+}
diff --git a/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java b/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java
index 675b0233d5d..a244f44a2d4 100644
--- a/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java
+++ b/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java
@@ -20,6 +20,7 @@ package org.apache.cxf.attachment;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.PushbackInputStream;
 import java.nio.charset.StandardCharsets;
@@ -667,6 +668,28 @@ public class AttachmentDeserializerTest {
         ins.close();
     }
 
+    @Test
+    public void testManyAttachmentHeaders() throws Exception {
+        StringBuilder sb = new StringBuilder(10000);
+        // Add many attachment headers
+        sb.append("------=_Part_34950_1098328613.1263781527359\n");
+        IntStream.range(0, 1000).forEach(i -> sb.append("Header-").append(i).append(": foo").append(i).append('\n'));
+        sb.append("Content-Type: text/xml; charset=UTF-8\n")
+            .append("Content-Transfer-Encoding: binary\n")
+            .append("Content-Id: <31...@auhpap02>\n")
+            .append('\n')
+            .append("<envelope/>\n");
+
+        msg = new MessageImpl();
+        msg.setContent(InputStream.class, new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8)));
+        msg.put(Message.CONTENT_TYPE, "multipart/related");
+        AttachmentDeserializer ad = new AttachmentDeserializer(msg);
+
+        assertThrows("Failure expected on too many attachment headers", IOException.class, 
+            () -> ad.initializeAttachments());
+    }
+
+    
     @Test
     public void testManyAttachments() throws Exception {
         StringBuilder sb = new StringBuilder(1000);


(cxf) 03/03: Add sensible default value to attachment-max-size property (#3188)

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 35d62a6d4a3fc369f95f87367f25332895db20c2
Author: Andriy Redko <dr...@gmail.com>
AuthorDate: Mon Jun 8 12:05:45 2026 -0400

    Add sensible default value to attachment-max-size property (#3188)
    
    * Add sensible default value to attachment-max-size property
    
    * Address code review comments
---
 .../cxf/attachment/AttachmentDeserializer.java     |  2 +
 .../org/apache/cxf/attachment/AttachmentUtil.java  | 33 ++++++++-------
 .../cxf/attachment/AttachmentDeserializerTest.java | 22 ++++++++++
 .../jaxrs/provider/FormEncodingProviderTest.java   | 49 ++++++++++++++++++++++
 4 files changed, 90 insertions(+), 16 deletions(-)

diff --git a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
index dfe76e6b4b1..71fdbd080ba 100644
--- a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
+++ b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java
@@ -62,6 +62,8 @@ public class AttachmentDeserializer {
      * The maximum size of the attachment. Allowed value is any of {@link Number} or {@link String}.
      */
     public static final String ATTACHMENT_MAX_SIZE = "attachment-max-size";
+    public static final long DEFAULT_ATTACHMENT_MAX_SIZE =
+        SystemPropertyAction.getInteger("org.apache.cxf.attachment-max-size", 50 * 1024 * 1024 /* 50 Mb */);
 
     /**
      * The maximum number of attachments permitted in a message. The default is 50.
diff --git a/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java b/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java
index f8fdd69216c..ff4bc10584c 100644
--- a/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java
+++ b/core/src/main/java/org/apache/cxf/attachment/AttachmentUtil.java
@@ -215,24 +215,25 @@ public final class AttachmentUtil {
         }
 
         Object maxSize = message.getContextualProperty(AttachmentDeserializer.ATTACHMENT_MAX_SIZE);
-        if (maxSize != null) {
-            if (maxSize instanceof Number) {
-                long size = ((Number) maxSize).longValue();
-                if (size >= 0) {
-                    bos.setMaxSize(size);
-                } else {
-                    LOG.warning("Max size value overflowed long. Do not set max size!");
-                }
-            } else if (maxSize instanceof String) {
-                try {
-                    bos.setMaxSize(Long.parseLong((String) maxSize));
-                } catch (NumberFormatException e) {
-                    throw new IOException("Provided threshold String is not a number", e);
-                }
+        if (maxSize == null) {
+            maxSize = AttachmentDeserializer.DEFAULT_ATTACHMENT_MAX_SIZE;
+        }
+        if (maxSize instanceof Number) {
+            long size = ((Number) maxSize).longValue();
+            if (size >= 0) {
+                bos.setMaxSize(size);
             } else {
-                throw new IOException("The value set as " + AttachmentDeserializer.ATTACHMENT_MAX_SIZE
-                        + " should be either an instance of Number or String");
+                LOG.warning("The max size value is set to unlimited.");
             }
+        } else if (maxSize instanceof String) {
+            try {
+                bos.setMaxSize(Long.parseLong((String) maxSize));
+            } catch (NumberFormatException e) {
+                throw new IOException("Provided max size String is not a number", e);
+            }
+        } else {
+            throw new IOException("The value set as " + AttachmentDeserializer.ATTACHMENT_MAX_SIZE
+                    + " should be either an instance of Number or String");
         }
     }
 
diff --git a/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java b/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java
index a244f44a2d4..7f1c3c7d50c 100644
--- a/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java
+++ b/core/src/test/java/org/apache/cxf/attachment/AttachmentDeserializerTest.java
@@ -31,7 +31,9 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import java.util.stream.IntStream;
+import java.util.stream.LongStream;
 
 import javax.activation.DataSource;
 import javax.activation.URLDataSource;
@@ -41,6 +43,7 @@ import javax.xml.parsers.SAXParserFactory;
 import org.xml.sax.helpers.DefaultHandler;
 
 import org.apache.cxf.helpers.IOUtils;
+import org.apache.cxf.io.CacheSizeExceededException;
 import org.apache.cxf.message.Attachment;
 import org.apache.cxf.message.Exchange;
 import org.apache.cxf.message.ExchangeImpl;
@@ -377,6 +380,25 @@ public class AttachmentDeserializerTest {
         assertEquals(-1, m.read(new byte[1000]));
         m.close();
     }
+    
+    @Test
+    public void testDefaultAttachmentMaxSize() throws Exception {
+        final byte[] messageBytes = ("------=_Part_1\n\nJJJJ\n------=_Part_1\n\n"
+                + "Content-Transfer-Encoding: binary\n\n" +  LongStream
+                    .range(0, AttachmentDeserializer.DEFAULT_ATTACHMENT_MAX_SIZE / 3 + 1)
+                    .mapToObj(i -> "=3D")
+                    .collect(Collectors.joining())
+                + "\n------=_Part_1\n").getBytes();
+
+        msg = new MessageImpl();
+        msg.setContent(InputStream.class, new ByteArrayInputStream(messageBytes));
+        msg.put(Message.CONTENT_TYPE, "multipart/related");
+        AttachmentDeserializer ad = new AttachmentDeserializer(msg);
+        ad.initializeAttachments();
+
+        // Force it to load the attachments
+        assertThrows(CacheSizeExceededException.class, () -> msg.getAttachments().size());
+    }
 
     @Test
     public void testCXF2542() throws Exception {
diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/FormEncodingProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/FormEncodingProviderTest.java
index aa225d34582..99b44bfcfa0 100644
--- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/FormEncodingProviderTest.java
+++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/FormEncodingProviderTest.java
@@ -25,20 +25,35 @@ import java.io.InputStream;
 import java.lang.annotation.Annotation;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.Encoded;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Form;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
 import javax.ws.rs.core.MultivaluedMap;
 
+import org.apache.cxf.Bus;
+import org.apache.cxf.attachment.AttachmentDeserializer;
+import org.apache.cxf.bus.extension.ExtensionManagerBus;
+import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
 import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.jaxrs.model.ProviderInfo;
 import org.apache.cxf.jaxrs.utils.HttpUtils;
+import org.apache.cxf.jaxrs.utils.InjectionUtils;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageImpl;
 
 import org.junit.Test;
 
+import static org.hamcrest.CoreMatchers.equalTo;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -142,6 +157,40 @@ public class FormEncodingProviderTest {
         assertEquals("Wrong entry for baz", "4", mvMap.getFirst("baz"));
 
     }
+    
+    @Test
+    public void testWriteMultipartTooLarge() throws Exception {
+        final MultivaluedMap<String, String> headers = new MultivaluedHashMap<>();
+        headers.add("Content-Transfer-Encoding", "binary");
+
+        final Bus bus = new ExtensionManagerBus();
+        final Message m = new MessageImpl();
+        m.put(AttachmentDeserializer.ATTACHMENT_PART_HEADERS, headers);
+        m.put(Message.CONTENT_TYPE, "multipart/related");
+
+        final ExchangeImpl exchange = new ExchangeImpl();
+        m.setExchange(exchange);
+        exchange.setInMessage(m);
+
+        FormEncodingProvider<MultipartBody> ferp = new FormEncodingProvider<>();
+        InjectionUtils.injectContextFields(ferp, new ProviderInfo<>(ferp, bus, false), m);
+        
+        final byte[] messageBytes = ("------=_Part_1\n\nJJJJ\n------=_Part_1\n\n"
+                + "Content-Transfer-Encoding: binary\n\n" +  LongStream
+                    .range(0, AttachmentDeserializer.DEFAULT_ATTACHMENT_MAX_SIZE / 3 + 1)
+                    .mapToObj(i -> "=3D")
+                    .collect(Collectors.joining())
+                + "\n------=_Part_1\n").getBytes();
+        try (ByteArrayInputStream in = new ByteArrayInputStream(messageBytes)) {
+            m.setContent(InputStream.class, in);
+   
+            final WebApplicationException ex = assertThrows(WebApplicationException.class, 
+                () -> ferp.readFrom(MultipartBody.class, null, new Annotation[]{}, 
+                    MediaType.MULTIPART_FORM_DATA_TYPE, null, in));
+            
+            assertThat(ex.getResponse().getStatus(), equalTo(413) /* Request Entity Too Large */); 
+        }
+    }
 
     @Test
     public void testWriteMultipleValues() throws Exception {