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)

Reply via email to