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 <[email protected]>
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) <[email protected]>
* 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 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../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 {