shashank created CAMEL-26306:
--------------------------------
Summary: 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
{{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)