[
https://issues.apache.org/jira/browse/CAMEL-23064?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Guillaume Nodet closed CAMEL-23064.
-----------------------------------
Fix Version/s: 4.19.0
Resolution: Fixed
> camel-as2 - HttpMessageUtils.extractEdiPayload() rejects valid AS2 messages
> with non-standard content types (text/plain, application/octet-stream, etc.)
> --------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-23064
> URL: https://issues.apache.org/jira/browse/CAMEL-23064
> Project: Camel
> Issue Type: Bug
> Components: camel-as2
> Affects Versions: 4.18.0
> Reporter: Reto Peter
> Assignee: Guillaume Nodet
> Priority: Minor
> Fix For: 4.19.0
>
> Attachments: HttpMessageUtils.java
>
>
> {panel:title=Problem}
> {{HttpMessageUtils.extractEdiPayload()}} in {{camel-as2-api}} strictly
> validates inner content types and rejects anything other than
> {{application/edifact}}, {{application/edi-x12}}, or
> {{application/edi-consent}}. Real-world AS2 partners frequently send EDI
> payloads wrapped in {{text/plain}}, {{application/octet-stream}},
> {{application/xml}}, etc.
> This causes a 500 error with no MDN returned, which violates the AS2 protocol
> (RFC 4130) requirement to always return an MDN when one is requested.
> {panel}
> h3. Steps to Reproduce
> # Configure a Camel AS2 server endpoint with encryption and signing enabled
> # Have a partner send an AS2 message where the inner payload (after
> decryption and signature verification) has content type {{text/plain;
> charset=US-ASCII}} instead of {{application/edifact}}
> # Observe: server returns HTTP 500 with no MDN
> This is common with partners using AS2Gateway, SAP, Mendelson, and other AS2
> implementations that do not set strict EDI content types on the inner payload.
> h3. Error Message
> {code}
> org.apache.hc.core5.http.HttpException: Failed to extract EDI payload:
> invalid content type 'text/plain; charset=US-ASCII' for AS2 compressed and
> signed entity
> at
> o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayloadFromEnvelopedEntity(HttpMessageUtils.java:261)
> at
> o.a.c.component.as2.api.util.HttpMessageUtils.extractEnvelopedData(HttpMessageUtils.java:178)
> at
> o.a.c.component.as2.api.util.HttpMessageUtils.extractEdiPayload(HttpMessageUtils.java:145)
> {code}
> h3. Root Cause
> {{HttpMessageUtils}} has 6 locations across 3 methods that throw
> {{HttpException}} when the inner entity is not an {{ApplicationEntity}}. The
> AS2 protocol (RFC 4130) does not mandate specific inner content types — the
> content type of the EDI payload is between the trading partners and is not
> part of the AS2 transport specification.
> h4. Affected code locations
> 1. {{extractEdiPayload()}} — default case (top-level):
> {code:java}
> default:
> throw new HttpException("Failed to extract EDI message: invalid content
> type '"
> + contentType.getMimeType() + "' for AS2 request message");
> {code}
> 2. {{extractMultipartSigned()}} — else branch:
> {code:java}
> MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
> if (mimeEntity instanceof ApplicationEntity) {
> ediEntity = (ApplicationEntity) mimeEntity;
> } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity
> compressedDataEntity) {
> ediEntity = extractEdiPayloadFromCompressedEntity(...);
> } else {
> throw new HttpException("Failed to extract EDI payload: invalid content
> type '"
> + mimeEntity.getContentType() + "' for AS2 compressed and signed
> message");
> }
> {code}
> 3. {{extractEdiPayloadFromEnvelopedEntity()}} — MULTIPART_SIGNED else branch:
> {code:java}
> } else {
> throw new HttpException("Failed to extract EDI payload: invalid content
> type '"
> + mimeEntity.getContentType() + "' for AS2 compressed and signed
> entity");
> }
> {code}
> 4. {{extractEdiPayloadFromEnvelopedEntity()}} — default case:
> {code:java}
> default:
> throw new HttpException("Failed to extract EDI payload: invalid content
> type '"
> + contentType.getMimeType() + "' for AS2 enveloped entity");
> {code}
> 5. {{extractEdiPayloadFromCompressedEntity()}} — else branch:
> {code:java}
> } else {
> throw new HttpException("Failed to extract EDI payload: invalid content
> type '"
> + mimeEntity.getContentType() + "' for AS2 compressed and signed
> entity");
> }
> {code}
> 6. {{extractEdiPayloadFromCompressedEntity()}} — default case:
> {code:java}
> default:
> throw new HttpException("Failed to extract EDI payload: invalid content
> type '"
> + contentType.getMimeType() + "' for AS2 compressed entity");
> {code}
> h3. Secondary Impact: MDN MIC Computation Also Fails
> Even if the payload extraction were caught at a higher level, the MDN
> generation also fails. {{ResponseMDN}} (the httpProcessor interceptor) calls
> {{extractEdiPayloadFromEnvelopedEntity()}} during MIC computation via
> {{DispositionNotificationMultipartReportEntity}}. This hits the same content
> type rejection, causing the MDN generation to throw, which {{HttpService}}
> catches and converts to a 500 error response. The partner receives HTTP 500
> instead of a proper MDN.
> This means the fix must be in {{HttpMessageUtils}} itself — wrapping at
> higher levels cannot fully resolve the issue.
> h3. Proposed Fix
> Instead of throwing when the inner entity is not an {{ApplicationEntity}},
> wrap it in a {{GenericApplicationEntity}} that:
> * Stores the content bytes (for {{getEdiMessage()}})
> * Delegates {{writeTo()}} to the original {{MimeEntity}} to preserve the
> exact byte representation for correct MIC computation
> h4. New helper method to add
> {code:java}
> private static ApplicationEntity wrapMimeEntityAsApplication(MimeEntity
> mimeEntity) throws HttpException {
> try {
> String contentTypeString = mimeEntity.getContentType();
> ContentType contentType = contentTypeString != null
> ? ContentType.parse(contentTypeString)
> : ContentType.DEFAULT_TEXT;
> byte[] content;
> try (InputStream is = mimeEntity.getContent()) {
> content = is.readAllBytes();
> }
> return new GenericApplicationEntity(content, contentType, null,
> false, mimeEntity);
> } catch (Exception e) {
> throw new HttpException(
> "Failed to read EDI payload from non-standard content type '" +
> mimeEntity.getContentType() + "'", e);
> }
> }
> {code}
> h4. New inner class to add
> {code:java}
> /**
> * Concrete ApplicationEntity subclass for wrapping non-standard content
> types.
> * CRITICAL: writeTo() delegates to the original MimeEntity to preserve the
> exact byte
> * representation (including headers like Content-Transfer-Encoding). The MIC
> is computed
> * by hashing the writeTo() output, so it MUST match the bytes the sender
> signed.
> * Using ApplicationEntity's default writeTo() would produce different bytes
> (different
> * headers, canonical encoding), causing MIC mismatch errors at the partner.
> */
> private static class GenericApplicationEntity extends ApplicationEntity {
> private final MimeEntity originalEntity;
> GenericApplicationEntity(byte[] content, ContentType contentType, String
> transferEncoding,
> boolean isMainBody, MimeEntity originalEntity) {
> super(content, contentType, transferEncoding, isMainBody, null);
> this.originalEntity = originalEntity;
> }
> @Override
> public void writeTo(OutputStream outstream) throws IOException {
> if (originalEntity != null) {
> originalEntity.writeTo(outstream);
> } else {
> super.writeTo(outstream);
> }
> }
> @Override
> public void close() {
> }
> }
> {code}
> h4. Changes to existing code
> Replace each of the 6 throwing branches with a call to
> {{wrapMimeEntityAsApplication()}}. Example for {{extractMultipartSigned()}}:
> {code:diff}
> MimeEntity mimeEntity = multipartSignedEntity.getSignedDataEntity();
> if (mimeEntity instanceof ApplicationEntity) {
> ediEntity = (ApplicationEntity) mimeEntity;
> } else if (mimeEntity instanceof ApplicationPkcs7MimeCompressedDataEntity
> compressedDataEntity) {
> ediEntity =
> extractEdiPayloadFromCompressedEntity(compressedDataEntity,
> decrpytingAndSigningInfo, true);
> } else {
> - throw new HttpException(
> - "Failed to extract EDI payload: invalid content type '" +
> mimeEntity.getContentType()
> - + "' for AS2 compressed and signed message");
> + ediEntity = wrapMimeEntityAsApplication(mimeEntity);
> }
> {code}
> The same pattern applies to all other throwing branches. The top-level
> {{extractEdiPayload()}} default case additionally needs security checks
> before wrapping (if signing/encryption was expected but not found, that
> should still be an error).
> h3. Why {{writeTo()}} Delegation is Critical
> Without delegating {{writeTo()}} to the original entity, the MIC (Message
> Integrity Check) returned in the MDN will not match the partner's expected
> value:
> * The MIC is a SHA-256 hash of the {{writeTo()}} output of the signed data
> entity
> * {{ApplicationEntity.writeTo()}} produces different bytes than the original
> {{MimeEntity.writeTo()}} (different MIME headers, e.g., missing
> {{Content-Transfer-Encoding: 7bit}})
> * The partner compares the returned MIC with the MIC they computed before
> sending
> * A mismatch causes the partner to flag the transfer as compromised
> h3. Additional Note: Existing Bug in {{getEntity()}}
> While investigating this issue, we noticed a bug in
> {{HttpMessageUtils.getEntity()}}:
> {code:java}
> } else if (message instanceof BasicClassicHttpResponse httpResponse) {
> HttpEntity entity = httpResponse.getEntity();
> if (entity != null && type.isInstance(entity)) {
> type.cast(entity); // BUG: should be "return type.cast(entity);"
> }
> }
> {code}
> The response branch casts but does not return the entity. This means
> {{getEntity()}} always returns {{null}} for response messages.
> h3. Test Scenario
> Partner configuration: AS2 partner sending encrypted (enveloped-data) +
> signed messages where the inner payload content type is {{text/plain;
> charset=US-ASCII}}.
> Message structure:
> {code}
> application/pkcs7-mime; smime-type=enveloped-data (outer: encrypted)
> └─ multipart/signed (after decryption:
> signed)
> └─ text/plain; charset=US-ASCII (inner payload —
> rejected by Camel)
> {code}
> *Expected behavior:* Message accepted, EDI payload extracted, synchronous MDN
> returned with correct MIC.
> *Actual behavior (Camel 4.18.0):* HTTP 500, no MDN, partner retries.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)