Skip to content

fix: drop split CRLFCRLF terminator bytes from the header buffer#218

Open
spokodev wants to merge 1 commit into
fastify:mainfrom
spokodev:fix/headerparser-split-crlf-terminator
Open

fix: drop split CRLFCRLF terminator bytes from the header buffer#218
spokodev wants to merge 1 commit into
fastify:mainfrom
spokodev:fix/headerparser-split-crlf-terminator

Conversation

@spokodev

Copy link
Copy Markdown

When a part's \r\n\r\n header terminator is split across two push() calls as ...\r then \n\r\n, the parser leaves a stray \r on the last header value of that part.

Repro (public API)

const body = Buffer.from(
  '--BOUND\r\n' +
  'Content-Disposition: form-data; name="f"; filename="a.txt"\r\n' +
  'Content-Type: image/png\r\n\r\n' +
  'DATA\r\n--BOUND--\r\n', 'binary')

// split so the \r\n\r\n after image/png breaks as "...image/png\r" | "\n\r\n"
// whole input  -> file mimetype = "image/png"
// this split   -> file mimetype = "image/png\r"   (trailing 0x0D)

A direct HeaderParser repro: Content-Type: text/plain\r\n\r\n pushed as …text/plain\r then \n\r\n yields the value text/plain\r.

Cause (deps/dicer/lib/HeaderParser.js)

On the first push, no terminator is found, so the whole chunk — including the leading \r of the terminator — is appended to this.buffer. On the next push the split-terminator branch matches (tail ends with a prefix of \r\n\r\n, data starts with the rest), sets appendEnd = 0 and finishes, but never removes the \r already in this.buffer. _parseHeader only strips full \r\n line terminators, so the lone trailing \r stays glued to the last header value.

Impact

Parsing becomes dependent on transport chunking: the same bytes give a different result depending on where the TCP/HTTP stream is split. The last header of any part (commonly Content-Type/mimetype, also content-transfer-encoding and the RFC 5987 filename*) intermittently carries a trailing \r. Code that compares or switches on the mimetype, or builds a filename, then gets silently wrong data on a fraction of requests. Pure 1-byte chunking does not trigger it (the tail only ever holds one byte), which is why the existing tests missed it.

Fix

Track how many terminator-prefix bytes were buffered on the previous push (splitTail) and trim them from this.buffer before finishing. splitTail is 0 in the common case, so the slice is a no-op there (no new branch, branchless to keep coverage intact).

Tests

Added a dicer-headerparser case for the ...\r | \n\r\n split asserting the value has no trailing \r. It fails on the current code and passes with the fix. Full unit suite green (211 passing) with coverage above the configured thresholds; an exhaustive 2-chunk split of the repro body is now invariant at every split point.

When a part's `\r\n\r\n` header terminator is split across two pushes as
`...\r` followed by `\n\r\n`, HeaderParser appended the leading `\r` to
the header buffer on the first push, then matched the terminator on the
second push without removing it. The stray `\r` stayed attached to the
last header value, so e.g. `Content-Type: image/png` was reported as
`image/png\r`.

This made parsing depend on transport chunking: the same bytes produced
a different result depending on where the stream was split, corrupting
the last header (mimetype, content-transfer-encoding, `filename*`) of any
part whenever the terminator happened to break right after the first CR.

Track how many terminator-prefix bytes were buffered on the previous
push and trim them before finishing. The count is 0 in the common case,
so the slice is a no-op there.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant