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 <[email protected]> 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: <318731183421.1263781527359.IBM.WEBSERVICES@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);
