[
https://issues.apache.org/jira/browse/CAMEL-26306?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Work on CAMEL-26306 started by shashank.
----------------------------------------
> camel-as2 - a CRLF whose CR is the last byte of an 8 KB read of the line
> reader loses its CR: the received EDI message changes and its signature fails
> ------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-26306
> URL: https://issues.apache.org/jira/browse/CAMEL-26306
> Project: Camel
> Issue Type: Bug
> Components: camel-as2
> Reporter: shashank
> Assignee: shashank
> Priority: Major
>
> {{AS2SessionInputBuffer.readLine}} (camel-as2-api) reports a line as ending
> with CRLF ({{isLastLineReadEnrichedByCarriageReturn()}}) only when the CR is
> in the read buffer right before the LF: {{if (i > 0 && this.buffer[i - 1] ==
> CR)}} (main 72a1bff606a3, line 189). When the CR is the last byte of one read
> and the LF the first byte of the next read, the CR is already in the line
> buffer: the line itself is right ({{lineFromLineBuffer}} strips the CR), but
> the line is reported as ending with a bare LF.
> {{EntityParser.parseBodyPartText}} rebuilds the content of a body part from
> the lines and these two flags (since CAMEL-15111, Camel 3.18.5 and 4.0, so
> that LF and CRLF line endings are kept as received; the flag and its check
> came with the same change). So that CRLF becomes LF in the content: the EDI
> message of an AS2 message ({{parseEDIEntityBody}} -> {{parseBodyPartBytes}}
> -> {{parseBodyPartText}}), a text/plain part (the human readable part of an
> MDN), and the other parts read through {{parseBodyPartBytes}}. When that CRLF
> is the one before a delimiter line, {{parseBodyPartText}} removes two
> characters for it ({{setLength(length - 2)}}), so the last byte of the
> content is lost instead.
> *When*: the line reader reads the entity content 8 KB at a time
> ({{doParseAS2MessageEntity}}: {{new AS2SessionInputBuffer(..., 8 * 1024)}}).
> The AS2 server parses the received body from a {{ByteArrayInputStream}}
> ({{AS2BHttpServerConnection.receiveRequestEntity}}), and the decrypted or
> decompressed content of an encrypted or compressed message is parsed the same
> way ({{EntityParser.parseEntity}}). So every CRLF whose CR is the last byte
> of an 8 KB read (offset 8191, 16383, ... of the body, or of the decrypted
> content) loses its CR. With 80 byte lines about one read in 80 ends that way,
> so an EDI file of 1 MB (128 reads) is more likely than not to be changed. The
> client parses an MDN response straight from the connection, where a read can
> end at any byte (not tested).
> *Effect*: for a signed message the signature is verified over the parsed
> content ({{SigningUtils.isValid}}), so verification fails and the message is
> rejected ("Failed to validate the signature") when
> {{validateSigningCertificateChain}} is set. For an unsigned message the route
> gets the changed content; the MIC in the MDN is computed over the changed
> content too ({{MicUtils.createReceivedContentMic}}), so it does not match the
> sender's MIC (reasoned from the code, not tested).
> *Reproduced* (camel-as2-api, main): an unsigned {{application/edifact}}
> request through {{EntityParser.parseAS2MessageEntity}} (the call the server
> connection makes) whose first line is 8191 bytes long:
> {{ApplicationEntity.getEdiMessage()}} is one byte shorter, the CR at offset
> 8191 is missing. With a first line of 8190 or 8192 bytes the content is
> unchanged. A {{multipart/signed}} EDI message whose body has a CR at offset
> 8191 (inside the EDI content, or before the delimiter line after it):
> {{SigningUtils.isValid}} on the parsed message returns false.
> *Fix*: when the LF is the first byte of the read buffer, take the CR from the
> end of the line buffer; otherwise the byte before the LF must be in the read
> buffer of this line ({{i > bufferpos}}), so a CR consumed before the line by
> {{read(InputStream)}} is no longer taken for its line ending either.
> *Tests*: new {{AS2SessionInputBufferTest}} (a CRLF split between two reads of
> a chunked stream, a CR that is the last byte of a full read buffer, a CR
> consumed by {{read}} before an empty line),
> {{EntityParserContentLineEndingTest.parseEdiMessageKeepsACrLfSplitByTheReadBuffer}}
> and {{signedEdiMessageWithACrLfSplitByTheReadBufferKeepsItsSignature}} (2
> cases; the reproducers above): all six fail on main and pass with the fix.
> camel-as2-api 117 tests and camel-as2-component 157 tests (including the
> in-JVM *IT classes; 1 skipped) pass.
> *How it was found*: an OSS-Fuzz-style harness for the MIME boundary handling
> Camel does itself ({{EntityParser.parseBodyPartText}} on
> {{AS2SessionInputBuffer}}; property: the text before the first delimiter
> line, whatever the read chunking), run on HotSpot with seeded random and
> grammar-aware inputs (no Jazzer), found it in the first second (minimised: a
> body of CR and LF in two reads gives LF). The Rocq JVM model (a JVM semantics
> in Rocq that executes the real class files) then states it as kernel-checked
> theorems on the class files of main and of the fix:
> {{main_split_crlf_reported_as_lf}} ("a CR LF b CR LF" read as "a CR" + "LF b
> CR LF": the first line is reported with a bare LF) and
> {{fix_split_crlf_reported_as_crlf}}. HotSpot gives the same results on the
> same class files.
> *Affected*: main, camel-4.22.x, camel-4.18.x, camel-4.14.x and camel-4.10.x
> have the same check (read, not run); on 4.18.x and older the server parses
> the body straight from the connection, so there a read can end at any byte.
> The fix is local to {{readLine}} and changes no option or default, so it can
> be backported.
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)