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 dc0e735315a..e182e878f17 100644 --- a/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java +++ b/core/src/main/java/org/apache/cxf/attachment/AttachmentDeserializer.java @@ -68,6 +68,7 @@ public class AttachmentDeserializer { * The maximum number of attachments permitted in a message. The default is 50. */ public static final String ATTACHMENT_MAX_COUNT = "attachment-max-count"; + public static final int DEFAULT_ATTACHMENT_MAX_COUNT = 50; /** * The maximum number of attachment headers permitted in a message. The default is 500. @@ -133,7 +134,7 @@ public void initializeAttachments() throws IOException { initializeRootMessage(); Object maxCountProperty = message.getContextualProperty(AttachmentDeserializer.ATTACHMENT_MAX_COUNT); - int maxCount = 50; + int maxCount = DEFAULT_ATTACHMENT_MAX_COUNT; if (maxCountProperty != null) { if (maxCountProperty instanceof Integer) { maxCount = (Integer)maxCountProperty; diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/ext/MessageContextImpl.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/ext/MessageContextImpl.java index ad74ad85ca1..38322fec0fd 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/ext/MessageContextImpl.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/ext/MessageContextImpl.java @@ -273,6 +273,8 @@ private MultipartBody createAttachments(String propertyName) { m.getExchange().getInMessage().get(AttachmentDeserializer.ATTACHMENT_MAX_SIZE)); inMessage.put(AttachmentDeserializer.ATTACHMENT_MAX_HEADER_SIZE, m.getExchange().getInMessage().get(AttachmentDeserializer.ATTACHMENT_MAX_HEADER_SIZE)); + inMessage.put(AttachmentDeserializer.ATTACHMENT_MAX_COUNT, + m.getExchange().getInMessage().get(AttachmentDeserializer.ATTACHMENT_MAX_COUNT)); inMessage.setContent(InputStream.class, m.getExchange().getInMessage().get("org.apache.cxf.multipart.embedded.input")); inMessage.put(Message.CONTENT_TYPE, @@ -282,6 +284,16 @@ private MultipartBody createAttachments(String propertyName) { new AttachmentInputInterceptor().handleMessage(inMessage); + final Object maxCountProperty = inMessage.getContextualProperty(AttachmentDeserializer.ATTACHMENT_MAX_COUNT); + int maxAttachmentCount = AttachmentDeserializer.DEFAULT_ATTACHMENT_MAX_COUNT; + if (maxCountProperty != null) { + if (maxCountProperty instanceof Integer) { + maxAttachmentCount = (Integer)maxCountProperty; + } else { + maxAttachmentCount = Integer.parseInt((String)maxCountProperty); + } + } + List newAttachments = new LinkedList<>(); try { Map> headers @@ -293,6 +305,9 @@ private MultipartBody createAttachments(String propertyName) { inMessage), new ProvidersImpl(inMessage)); newAttachments.add(first); + if (newAttachments.size() > maxAttachmentCount) { + throw new IOException("The message contains more attachments than are permitted"); + } } catch (IOException ex) { throw ExceptionUtils.toInternalServerErrorException(ex, null); } @@ -302,9 +317,17 @@ private MultipartBody createAttachments(String propertyName) { if (childAttachments == null) { childAttachments = Collections.emptyList(); } - for (org.apache.cxf.message.Attachment a : childAttachments) { - newAttachments.add(new Attachment(a, new ProvidersImpl(inMessage))); + try { + for (org.apache.cxf.message.Attachment a : childAttachments) { + newAttachments.add(new Attachment(a, new ProvidersImpl(inMessage))); + if (newAttachments.size() > maxAttachmentCount) { + throw new IOException("The message contains more attachments than are permitted"); + } + } + } catch (IOException ex) { + throw ExceptionUtils.toInternalServerErrorException(ex, null); } + MediaType mt = embeddedAttachment ? (MediaType)inMessage.get("org.apache.cxf.multipart.embedded.ctype") : getHttpHeaders().getMediaType(); diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/EntityPartProvider.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/EntityPartProvider.java index 5fcb68da9a8..e2be13b641b 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/EntityPartProvider.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/provider/EntityPartProvider.java @@ -73,6 +73,14 @@ public class EntityPartProvider extends AbstractConfigurableProvider private String attachmentDir; private String attachmentThreshold; private String attachmentMaxSize; + + void setProviders(Providers providers) { + this.providers = providers; + } + + void setMessageContext(MessageContext context) { + this.mc = context; + } public void setAttachmentDirectory(String dir) { attachmentDir = dir; diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/EntityPartProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/EntityPartProviderTest.java new file mode 100644 index 00000000000..098009366ef --- /dev/null +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/EntityPartProviderTest.java @@ -0,0 +1,170 @@ +/** + * 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.jaxrs.provider; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.IntStream; + +import jakarta.ws.rs.core.EntityPart; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Providers; +import org.apache.cxf.attachment.AttachmentDeserializer; +import org.apache.cxf.jaxrs.ext.MessageContextImpl; +import org.apache.cxf.jaxrs.impl.MetadataMap; +import org.apache.cxf.message.Exchange; +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.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class EntityPartProviderTest { + private final Providers providers = new Providers() { + @Override + public MessageBodyReader getMessageBodyReader(Class type, + Type genericType, Annotation[] annotations, MediaType mediaType) { + return new BinaryDataProvider<>(); + } + + @Override + public MessageBodyWriter getMessageBodyWriter(Class type, + Type genericType, Annotation[] annotations, MediaType mediaType) { + throw new UnsupportedOperationException(); + } + + @Override + public ExceptionMapper getExceptionMapper(Class type) { + throw new UnsupportedOperationException(); + } + + @Override + public ContextResolver getContextResolver(Class contextType, MediaType mediaType) { + throw new UnsupportedOperationException(); + } + + }; + + @SuppressWarnings("unchecked") + @Test + public void testChangingMaxAttachmentCount() throws Exception { + final Exchange exchange = new ExchangeImpl(); + final EntityPartProvider p = new EntityPartProvider(); + + StringBuilder sb = new StringBuilder(1000); + sb.append("SomeHeader: foo\n") + .append("------=_Part_34950_1098328613.1263781527359\n") + .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("\n"); + + // Add many attachments + IntStream.range(0, 40).forEach(i -> { + sb.append("------=_Part_34950_1098328613.1263781527359\n") + .append("Content-Type: text/xml\n") + .append("Content-Transfer-Encoding: binary\n") + .append("Content-Id: \n") + .append('\n') + .append("\n") + .append("------=_Part_34950_1098328613.1263781527359--\n"); + }); + + // Too many attachments we'll not allow it + final Message msg = new MessageImpl(); + msg.setExchange(exchange); + exchange.setInMessage(msg); + p.setMessageContext(new MessageContextImpl(msg)); + p.setProviders(providers); + + msg.put(AttachmentDeserializer.ATTACHMENT_MAX_COUNT, "30"); + msg.setContent(InputStream.class, new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8))); + msg.put(Message.CONTENT_TYPE, "multipart/related"); + + assertThrows("Failure expected on too many attachments", RuntimeException.class, + () -> p.readFrom((Class>) (Class) List.class, EntityPart.class, new Annotation[]{}, + MediaType.APPLICATION_OCTET_STREAM_TYPE, + new MetadataMap(), + msg.getContent(InputStream.class))); + + // Now we'll allow it + final Message msg2 = new MessageImpl(); + msg2.setExchange(exchange); + exchange.setInMessage(msg2); + p.setMessageContext(new MessageContextImpl(msg2)); + + msg2.put(AttachmentDeserializer.ATTACHMENT_MAX_COUNT, "60"); + msg2.setContent(InputStream.class, new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8))); + msg2.put(Message.CONTENT_TYPE, "multipart/related"); + + List body = p.readFrom((Class>) (Class) List.class, + EntityPart.class, new Annotation[]{}, + MediaType.APPLICATION_OCTET_STREAM_TYPE, + new MetadataMap(), + msg2.getContent(InputStream.class)); + + // Force it to load the attachments + assertEquals(41, body.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void testManyAttachmentHeaders() throws Exception { + final Exchange exchange = new ExchangeImpl(); + final EntityPartProvider p = new EntityPartProvider(); + + 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("\n"); + + final Message msg = new MessageImpl(); + msg.setExchange(exchange); + exchange.setInMessage(msg); + p.setMessageContext(new MessageContextImpl(msg)); + + msg.setContent(InputStream.class, new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8))); + msg.put(Message.CONTENT_TYPE, "multipart/related"); + + assertThrows("Failure expected on too many attachment headers", RuntimeException.class, + () -> p.readFrom((Class>) (Class) List.class, + EntityPart.class, new Annotation[]{}, + MediaType.APPLICATION_OCTET_STREAM_TYPE, + new MetadataMap(), + msg.getContent(InputStream.class))); + } +} diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MultipartProviderTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MultipartProviderTest.java new file mode 100644 index 00000000000..e3712fb6b31 --- /dev/null +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/provider/MultipartProviderTest.java @@ -0,0 +1,133 @@ +/** + * 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.jaxrs.provider; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.IntStream; + +import jakarta.ws.rs.core.MediaType; +import org.apache.cxf.attachment.AttachmentDeserializer; +import org.apache.cxf.jaxrs.ext.MessageContextImpl; +import org.apache.cxf.jaxrs.impl.MetadataMap; +import org.apache.cxf.message.Exchange; +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.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class MultipartProviderTest { + @Test + public void testChangingMaxAttachmentCount() throws Exception { + final Exchange exchange = new ExchangeImpl(); + final MultipartProvider p = new MultipartProvider(); + + StringBuilder sb = new StringBuilder(1000); + sb.append("SomeHeader: foo\n") + .append("------=_Part_34950_1098328613.1263781527359\n") + .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("\n"); + + // Add many attachments + IntStream.range(0, 40).forEach(i -> { + sb.append("------=_Part_34950_1098328613.1263781527359\n") + .append("Content-Type: text/xml\n") + .append("Content-Transfer-Encoding: binary\n") + .append("Content-Id: \n") + .append('\n') + .append("\n") + .append("------=_Part_34950_1098328613.1263781527359--\n"); + }); + + // Too many attachments we'll not allow it + final Message msg = new MessageImpl(); + msg.setExchange(exchange); + exchange.setInMessage(msg); + p.setMessageContext(new MessageContextImpl(msg)); + + msg.put(AttachmentDeserializer.ATTACHMENT_MAX_COUNT, "30"); + msg.setContent(InputStream.class, new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8))); + msg.put(Message.CONTENT_TYPE, "multipart/related"); + + assertThrows("Failure expected on too many attachments", RuntimeException.class, + () -> p.readFrom(Object.class, Object.class, new Annotation[]{}, + MediaType.APPLICATION_OCTET_STREAM_TYPE, + new MetadataMap(), + msg.getContent(InputStream.class))); + + // Now we'll allow it + final Message msg2 = new MessageImpl(); + msg2.setExchange(exchange); + exchange.setInMessage(msg2); + p.setMessageContext(new MessageContextImpl(msg2)); + + msg2.put(AttachmentDeserializer.ATTACHMENT_MAX_COUNT, "60"); + msg2.setContent(InputStream.class, new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8))); + msg2.put(Message.CONTENT_TYPE, "multipart/related"); + + Map body = (Map) p.readFrom(Object.class, Object.class, new Annotation[]{}, + MediaType.APPLICATION_OCTET_STREAM_TYPE, + new MetadataMap(), + msg2.getContent(InputStream.class)); + + // Force it to load the attachments + assertEquals(3, body.size()); + } + + @Test + public void testManyAttachmentHeaders() throws Exception { + final Exchange exchange = new ExchangeImpl(); + final MultipartProvider p = new MultipartProvider(); + + 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("\n"); + + final Message msg = new MessageImpl(); + msg.setExchange(exchange); + exchange.setInMessage(msg); + p.setMessageContext(new MessageContextImpl(msg)); + + msg.setContent(InputStream.class, new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8))); + msg.put(Message.CONTENT_TYPE, "multipart/related"); + + assertThrows("Failure expected on too many attachment headers", RuntimeException.class, + () -> p.readFrom(Object.class, Object.class, new Annotation[]{}, + MediaType.APPLICATION_OCTET_STREAM_TYPE, + new MetadataMap(), + msg.getContent(InputStream.class))); + } +}