From 7521feb506ef27c4708fda22ed56aa4fe78d84d9 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Tue, 23 Jun 2026 18:13:21 +0100 Subject: [PATCH 1/2] fix: drop split CRLFCRLF terminator bytes from the header buffer 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. --- deps/dicer/lib/HeaderParser.js | 7 +++++++ test/dicer-headerparser.test.js | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/deps/dicer/lib/HeaderParser.js b/deps/dicer/lib/HeaderParser.js index 92c4935..31ddfc3 100644 --- a/deps/dicer/lib/HeaderParser.js +++ b/deps/dicer/lib/HeaderParser.js @@ -29,6 +29,7 @@ HeaderParser.prototype.push = function (data) { let end = data.length let appendEnd = data.length let found = false + let splitTail = 0 const tail = this.tail for (let i = tail.length; i > 0; --i) { @@ -40,6 +41,7 @@ HeaderParser.prototype.push = function (data) { if (matched) { end = S_DCRLF.length - i appendEnd = 0 + splitTail = i found = true break } @@ -74,6 +76,11 @@ HeaderParser.prototype.push = function (data) { } if (found) { + // A CRLFCRLF header terminator split across pushes left its leading bytes + // in this.buffer on the previous push; drop them so they do not trail the + // last header value. splitTail is 0 in the common case, making this a no-op. + this.buffer = this.buffer.slice(0, this.buffer.length - splitTail) + this.nread -= splitTail this._finish() return end } diff --git a/test/dicer-headerparser.test.js b/test/dicer-headerparser.test.js index dd37ef0..f0a8bd8 100644 --- a/test/dicer-headerparser.test.js +++ b/test/dicer-headerparser.test.js @@ -19,6 +19,11 @@ test('dicer-headerparser', async t => { expected: { foo: ['bar'] }, what: 'Header terminator across chunks' }, + { + source: ['Foo: bar\r', '\n\r\n'], + expected: { foo: ['bar'] }, + what: 'Header terminator split between CR and LFCRLF leaves no trailing CR on the value' + }, { source: ['Foo: bar\r', '\n\r', '\nextra'], cfg: { From 2ce1b23ba67cb7320d878a8c34ead4d94c91808f Mon Sep 17 00:00:00 2001 From: Yarchik Date: Wed, 29 Jul 2026 23:43:48 +0100 Subject: [PATCH 2/2] fix: skip the split-terminator slice once maxHeaderSize truncation has dropped bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a header block is truncated at maxHeaderSize (this.maxed), the trailing `\r` of a split CRLFCRLF terminator was never appended, so the last splitTail bytes of the buffer are real header content, not the terminator prefix. Slicing them off then corrupts the final value (e.g. `bar` -> `ba`). Guard the slice on !this.maxed; splitTail is only the buffer's tail when nothing was dropped. Also drop `this.nread -= splitTail` — dead, _finish() sets this.nread = 0 next. Adds a dicer-headerparser case with a small maxHeaderSize to cover the maxed branch and keep coverage at 100%. --- deps/dicer/lib/HeaderParser.js | 7 +++++-- test/dicer-headerparser.test.js | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/deps/dicer/lib/HeaderParser.js b/deps/dicer/lib/HeaderParser.js index 31ddfc3..ff64e4b 100644 --- a/deps/dicer/lib/HeaderParser.js +++ b/deps/dicer/lib/HeaderParser.js @@ -79,8 +79,11 @@ HeaderParser.prototype.push = function (data) { // A CRLFCRLF header terminator split across pushes left its leading bytes // in this.buffer on the previous push; drop them so they do not trail the // last header value. splitTail is 0 in the common case, making this a no-op. - this.buffer = this.buffer.slice(0, this.buffer.length - splitTail) - this.nread -= splitTail + // Skip when maxed: once maxHeaderSize truncation dropped the trailing bytes, + // the last splitTail bytes are real header content, not the terminator prefix. + if (!this.maxed) { + this.buffer = this.buffer.slice(0, this.buffer.length - splitTail) + } this._finish() return end } diff --git a/test/dicer-headerparser.test.js b/test/dicer-headerparser.test.js index f0a8bd8..81afbc0 100644 --- a/test/dicer-headerparser.test.js +++ b/test/dicer-headerparser.test.js @@ -24,6 +24,14 @@ test('dicer-headerparser', async t => { expected: { foo: ['bar'] }, what: 'Header terminator split between CR and LFCRLF leaves no trailing CR on the value' }, + { + source: ['Foo: bar\r', '\n\r\n'], + cfg: { + maxHeaderSize: 8 + }, + expected: { foo: ['bar'] }, + what: 'Header terminator split after maxHeaderSize truncation keeps the value intact' + }, { source: ['Foo: bar\r', '\n\r', '\nextra'], cfg: {