[
https://issues.apache.org/jira/browse/CAMEL-23064?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen updated CAMEL-23064:
--------------------------------
Priority: Minor (was: Major)
> 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
> Priority: Minor
> 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)