You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2026/09/23 08:01:52 UTC
(camel) branch main updated: CAMEL-24431: camel-as2 - bound the expansion of a compressed entity (#26656)
This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 1776cbf60ddf CAMEL-24431: camel-as2 - bound the expansion of a compressed entity (#26656)
1776cbf60ddf is described below
commit 1776cbf60ddf92b8780f92d921543584ee2e47d1
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Wed Sep 23 10:01:45 2026 +0200
CAMEL-24431: camel-as2 - bound the expansion of a compressed entity (#26656)
* CAMEL-24431: camel-as2 - bound the expansion of a compressed entity
HttpMessageUtils.extractEdiPayloadFromCompressedEntity() expanded the entity
with an unbounded expander:
MimeEntity entity = compressedDataEntity.getCompressedEntity(new ZlibExpanderProvider());
The expansion happens while the payload is extracted, which is before the
signature has been established, so the work is done on behalf of a sender that
is not yet authenticated, and zlib reaches roughly 1000:1.
All four call sites are internal statics with no configuration threaded through,
and passing an endpoint option down would mean changing several public
signatures in camel-as2-api. Instead the bound is a constant,
MAX_COMPRESSED_ENTITY_EXPANSION, set well above any realistic EDI payload, with
a public overload taking an explicit bound for a deployment that needs a
different value.
Note on the test: it calls the new overload, so unlike the other fixes in this
series it cannot show the behaviour before the change - there was no bound to
pass. What it shows is that the mechanism works: refused at 1 KiB with
"Failed to decompress data" from EntityParser.uncompressData, and successful at
10 MiB.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
* CAMEL-24431: camel-as2 - bound the MIC-path decompression and address review nits
Addresses review feedback on #26656:
- MicUtils#findSignedDataEntity decompressed a compressed-data message with
an unbounded ZlibExpanderProvider on the same pre-authentication MIC path;
it now uses the MAX_COMPRESSED_ENTITY_EXPANSION bound, like HttpMessageUtils.
- added a MIC-path test that a within-bound compressed message still
decompresses and yields a MIC.
- de-duplicated a comment and replaced a fully-qualified StandardCharsets
with an import in the expansion-bound test.
Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
Signed-off-by: Andrea Cosentino <an...@gmail.com>
---------
Signed-off-by: Andrea Cosentino <an...@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../component/as2/api/util/HttpMessageUtils.java | 28 +++++++-
.../camel/component/as2/api/util/MicUtils.java | 3 +-
.../util/CompressedEntityExpansionBoundTest.java | 75 ++++++++++++++++++++++
.../camel/component/as2/api/util/MicUtilsTest.java | 25 ++++++++
4 files changed, 129 insertions(+), 2 deletions(-)
diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java
index 1cfc126efa2d..066ee8d9bfed 100644
--- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java
+++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/HttpMessageUtils.java
@@ -48,6 +48,13 @@ import org.slf4j.LoggerFactory;
public final class HttpMessageUtils {
+ /**
+ * Largest expansion accepted for a compressed AS2 entity. Chosen to be far above any realistic EDI payload while
+ * still bounding the pathological case; the overload taking an explicit bound is available where a deployment needs
+ * a different value.
+ */
+ public static final long MAX_COMPRESSED_ENTITY_EXPANSION = 100L * 1024 * 1024;
+
private static final Logger LOG = LoggerFactory.getLogger(HttpMessageUtils.class);
private HttpMessageUtils() {
@@ -335,9 +342,28 @@ public final class HttpMessageUtils {
ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity, DecrpytingAndSigningInfo decrpytingAndSigningInfo,
boolean hasValidSignature)
throws HttpException {
+ return extractEdiPayloadFromCompressedEntity(compressedDataEntity, decrpytingAndSigningInfo, hasValidSignature,
+ MAX_COMPRESSED_ENTITY_EXPANSION);
+ }
+
+ /**
+ * As
+ * {@link #extractEdiPayloadFromCompressedEntity(ApplicationPkcs7MimeCompressedDataEntity, DecrpytingAndSigningInfo, boolean)},
+ * with an explicit bound on how far the compressed entity may expand.
+ *
+ * @param maxExpandedSize the largest expansion to accept, in bytes
+ */
+ public static ApplicationEntity extractEdiPayloadFromCompressedEntity(
+ ApplicationPkcs7MimeCompressedDataEntity compressedDataEntity, DecrpytingAndSigningInfo decrpytingAndSigningInfo,
+ boolean hasValidSignature, long maxExpandedSize)
+ throws HttpException {
ApplicationEntity ediEntity;
- MimeEntity entity = compressedDataEntity.getCompressedEntity(new ZlibExpanderProvider());
+ // The expansion happens while extracting the payload, which is before the signature has been
+ // established, so the work is done on behalf of a sender that is not yet authenticated. zlib
+ // reaches roughly 1000:1, so an unbounded expander lets a small entity claim a large amount of
+ // memory. Bound it.
+ MimeEntity entity = compressedDataEntity.getCompressedEntity(new ZlibExpanderProvider(maxExpandedSize));
String contentTypeString = entity.getContentType();
if (contentTypeString == null) {
throw new HttpException("Failed to extract EDI payload: content type missing from compressed entity");
diff --git a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java
index 5e11779f6ca4..294e83b9b189 100644
--- a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java
+++ b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/util/MicUtils.java
@@ -176,7 +176,8 @@ public final class MicUtils {
= HttpMessageUtils.getEntity(request, ApplicationPkcs7MimeCompressedDataEntity.class);
if (compressedEntity != null) {
MimeEntity inner = compressedEntity
- .getCompressedEntity(new ZlibExpanderProvider());
+ .getCompressedEntity(
+ new ZlibExpanderProvider(HttpMessageUtils.MAX_COMPRESSED_ENTITY_EXPANSION));
if (inner instanceof MultipartSignedEntity signedEntity) {
return signedEntity.getSignedDataEntity();
}
diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/CompressedEntityExpansionBoundTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/CompressedEntityExpansionBoundTest.java
new file mode 100644
index 000000000000..c7f22a16cda5
--- /dev/null
+++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/CompressedEntityExpansionBoundTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.camel.component.as2.api.util;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.component.as2.api.entity.ApplicationEDIFACTEntity;
+import org.apache.camel.component.as2.api.entity.ApplicationEntity;
+import org.apache.camel.component.as2.api.entity.ApplicationPkcs7MimeCompressedDataEntity;
+import org.apache.hc.core5.http.HttpException;
+import org.bouncycastle.cms.CMSCompressedDataGenerator;
+import org.bouncycastle.cms.jcajce.ZlibCompressor;
+import org.bouncycastle.operator.OutputCompressor;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A compressed entity is expanded while the payload is extracted, which happens before the signature has been
+ * established. The expansion must therefore be bounded rather than proportional to whatever an unauthenticated sender
+ * claims.
+ */
+class CompressedEntityExpansionBoundTest {
+
+ /** Highly compressible, so the entity on the wire is a tiny fraction of what it expands to. */
+ private static final String PAYLOAD = "A".repeat(200_000);
+
+ private static final HttpMessageUtils.DecrpytingAndSigningInfo NO_SECURITY
+ = new HttpMessageUtils.DecrpytingAndSigningInfo(null, null);
+
+ @Test
+ void expansionBeyondTheBoundIsRefused() throws Exception {
+ ApplicationPkcs7MimeCompressedDataEntity compressed = compressedEntity();
+
+ // the failure must come from the expander refusing, not from anything downstream, so assert the type
+ HttpException thrown = assertThrows(HttpException.class,
+ () -> HttpMessageUtils.extractEdiPayloadFromCompressedEntity(compressed, NO_SECURITY, false, 1024L),
+ "expanding well past the bound must fail rather than allocate");
+ assertTrue(thrown.getMessage().contains("decompress"),
+ "expected the expander to refuse, but failed with: " + thrown.getMessage());
+ }
+
+ @Test
+ void expansionWithinTheBoundStillWorks() throws Exception {
+ ApplicationPkcs7MimeCompressedDataEntity compressed = compressedEntity();
+
+ ApplicationEntity entity = HttpMessageUtils.extractEdiPayloadFromCompressedEntity(
+ compressed, NO_SECURITY, false, 10L * 1024 * 1024);
+ assertInstanceOf(ApplicationEntity.class, entity);
+ }
+
+ private static ApplicationPkcs7MimeCompressedDataEntity compressedEntity() throws Exception {
+ ApplicationEDIFACTEntity ediEntity = new ApplicationEDIFACTEntity(
+ PAYLOAD.getBytes(StandardCharsets.US_ASCII), "US-ASCII", "7bit", false, null);
+ CMSCompressedDataGenerator generator = new CMSCompressedDataGenerator();
+ OutputCompressor compressor = new ZlibCompressor();
+ return new ApplicationPkcs7MimeCompressedDataEntity(ediEntity, generator, compressor, "base64", false);
+ }
+}
diff --git a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java
index 6892871d9714..678e1cd71431 100644
--- a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java
+++ b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/util/MicUtilsTest.java
@@ -25,10 +25,13 @@ import org.apache.camel.component.as2.api.AS2Header;
import org.apache.camel.component.as2.api.AS2MimeType;
import org.apache.camel.component.as2.api.AS2TransferEncoding;
import org.apache.camel.component.as2.api.entity.ApplicationEDIFACTEntity;
+import org.apache.camel.component.as2.api.entity.ApplicationPkcs7MimeCompressedDataEntity;
import org.apache.camel.component.as2.api.util.MicUtils.ReceivedContentMic;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.bouncycastle.cms.CMSCompressedDataGenerator;
+import org.bouncycastle.cms.jcajce.ZlibCompressor;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -108,6 +111,9 @@ public class MicUtilsTest {
private static final String EXPECTED_MESSAGE_DIGEST_ALGORITHM = "sha1";
private static final String EXPECTED_ENCODED_MESSAGE_DIGEST = "0mGTGdBjQtu8VQ52506Coi0xHbc=";
+ /** Highly compressible, so the compressed entity on the wire is a tiny fraction of what it expands to. */
+ private static final String COMPRESSIBLE_EDI_PAYLOAD = "A".repeat(200_000);
+
@BeforeEach
public void setUp() {
Security.addProvider(new BouncyCastleProvider());
@@ -142,6 +148,25 @@ public class MicUtilsTest {
"Unexpected encoded message digest value");
}
+ @Test
+ public void createReceivedContentMicForCompressedMessageStaysWithinBound() throws Exception {
+ // A compressed-data message is decompressed on the MIC path (MicUtils#findSignedDataEntity) before the sender
+ // has been authenticated, so that expansion is now bounded (CAMEL-24431). A payload well within the bound must
+ // still decompress and yield a MIC rather than being refused - exercising the newly bounded call site.
+ BasicClassicHttpRequest request = new BasicClassicHttpRequest("POST", "/");
+ request.addHeader(AS2Header.DISPOSITION_NOTIFICATION_OPTIONS, DISPOSITION_NOTIFICATION_OPTIONS_VALUE);
+
+ ApplicationEDIFACTEntity ediEntity = new ApplicationEDIFACTEntity(
+ COMPRESSIBLE_EDI_PAYLOAD.getBytes(StandardCharsets.US_ASCII), "US-ASCII", "7bit", false, null);
+ ApplicationPkcs7MimeCompressedDataEntity compressed = new ApplicationPkcs7MimeCompressedDataEntity(
+ ediEntity, new CMSCompressedDataGenerator(), new ZlibCompressor(), "base64", false);
+ request.addHeader(AS2Header.CONTENT_TYPE, compressed.getContentType());
+ request.setEntity(compressed);
+
+ ReceivedContentMic receivedContentMic = MicUtils.createReceivedContentMic(request, null, null);
+ assertNotNull(receivedContentMic, "a within-bound compressed message must still produce a MIC");
+ }
+
// verify that a MIC is calculated correctly for an EDI message containing non ASCII chars
@Test
public void createReceivedContentMicWithNonAsciiContentTest() throws Exception {