From 3044e7b58df05fca9f2fdc93a7454176a61a7fe7 Mon Sep 17 00:00:00 2001 From: DeepView Autofix <276251120+deepview-autofix@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:51:12 +0300 Subject: [PATCH] fix(fetch): use || for CRLF check in multipart formdata-parser The CRLF validation in `parseMultipartFormDataHeaders` and the content-disposition attribute loop incorrectly used `&&` instead of `||`. With `&&`, the parser only failed/terminated when *both* bytes mismatched CRLF, so a stray `\r` (not followed by `\n`) or `\n` (not preceded by `\r`) was silently accepted. This could drop subsequent attributes such as `filename` and misalign header/body boundary detection. The other CRLF checks in the same file already use `||`. Co-Authored-By: Claude Co-Authored-By: DeepView Autofix <276251120+deepview-autofix@users.noreply.github.com> Co-Authored-By: Nikita Skovoroda Signed-off-by: Nikita Skovoroda --- lib/web/fetch/formdata-parser.js | 6 +++--- test/fetch/formdata.js | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/web/fetch/formdata-parser.js b/lib/web/fetch/formdata-parser.js index b65848998f7..7723b5fe309 100644 --- a/lib/web/fetch/formdata-parser.js +++ b/lib/web/fetch/formdata-parser.js @@ -383,8 +383,8 @@ function parseMultipartFormDataHeaders (input, position) { // Parse attributes recursively until CRLF while ( position.position < input.length && - input[position.position] !== 0x0d && - input[position.position + 1] !== 0x0a + (input[position.position] !== 0x0d || + input[position.position + 1] !== 0x0a) ) { const attribute = parseContentDispositionAttribute(input, position) @@ -448,7 +448,7 @@ function parseMultipartFormDataHeaders (input, position) { // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A // (CR LF), return failure. Otherwise, advance position by 2 (past the newline). - if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) { + if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) { throw parsingError('expected CRLF') } else { position.position += 2 diff --git a/test/fetch/formdata.js b/test/fetch/formdata.js index 5c0b721d281..3ebfea5d6de 100644 --- a/test/fetch/formdata.js +++ b/test/fetch/formdata.js @@ -383,3 +383,26 @@ test('.formData() with multipart/form-data body that ends with --\r\n', async (t await request.formData() }) + +test('.formData() rejects malformed multipart header line ending with bare CR', async (t) => { + const boundary = '----formdata-undici-bare-cr-0000000000' + const body = Buffer.concat([ + Buffer.from('--' + boundary + '\r\n'), + Buffer.from('Content-Disposition: form-data; name="x"'), + Buffer.from([0x0d]), // bare CR (no LF) + Buffer.from('Content-Type: text/plain\r\n'), + Buffer.from('\r\n'), + Buffer.from('hello\r\n'), + Buffer.from('--' + boundary + '--\r\n') + ]) + + const request = new Request('http://localhost', { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data; boundary=' + boundary + }, + body + }) + + await t.assert.rejects(request.formData(), TypeError) +})