fix(core): Handle invalid percent sequences and equals signs in HTTP response headers (#27691)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon 2026-04-13 16:17:33 +01:00 committed by GitHub
parent 550409923a
commit ca71d89d88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 5 deletions

View file

@ -50,6 +50,17 @@ describe('parseContentType', () => {
},
description: 'should parse content type with multiple parameters',
},
{
input: 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk=',
expected: {
type: 'multipart/form-data',
parameters: {
charset: 'utf-8',
boundary: '----WebKitFormBoundary7MA4YWxk=',
},
},
description: 'should preserve trailing = in boundary parameter',
},
{
input: 'text/plain; charset="utf-8"; filename="test.txt"',
expected: {
@ -141,6 +152,21 @@ describe('parseContentDisposition', () => {
expected: { type: 'attachment', filename: '😀.txt' },
description: 'should handle encoded filenames',
},
{
input: 'attachment; filename="my_scan_144dpi_75%.pdf"',
expected: { type: 'attachment', filename: 'my_scan_144dpi_75%.pdf' },
description: 'should handle filenames with bare percent sign',
},
{
input: 'attachment; filename="report=final.pdf"',
expected: { type: 'attachment', filename: 'report=final.pdf' },
description: 'should handle filenames with equals sign',
},
{
input: 'attachment; filename="report 50% done.pdf"',
expected: { type: 'attachment', filename: 'report 50% done.pdf' },
description: 'should handle filenames with bare percent sign and space',
},
{
input: 'attachment; size=123; filename="test.txt"; creation-date="Thu, 1 Jan 2020"',
expected: { type: 'attachment', filename: 'test.txt' },

View file

@ -3,12 +3,37 @@ import type { IncomingMessage } from 'http';
function parseHeaderParameters(parameters: string[]): Record<string, string> {
return parameters.reduce(
(acc, param) => {
const [key, value] = param.split('=');
let decodedValue = decodeURIComponent(value).trim();
if (decodedValue.startsWith('"') && decodedValue.endsWith('"')) {
decodedValue = decodedValue.slice(1, -1);
const eqIdx = param.indexOf('=');
if (eqIdx === -1) return acc;
const key = param.slice(0, eqIdx);
let processedValue = param.slice(eqIdx + 1).trim();
if (processedValue.startsWith('"') && processedValue.endsWith('"')) {
// Quoted string: strip quotes first, then try to percent-decode.
// Some non-standard servers percent-encode inside quoted strings
// (e.g. filename="my%20file.pdf"). Per RFC 6266, quoted filename
// values are plain strings but we decode as a best-effort fallback.
// A bare % that isn't a valid percent-encoded sequence is kept as-is.
processedValue = processedValue.slice(1, -1);
try {
processedValue = decodeURIComponent(processedValue);
} catch {
// Keep raw value — contains an invalid percent sequence (e.g. 75%.pdf)
}
} else {
// Unquoted value: may be entirely percent-encoded, including the quotes
// themselves (e.g. filename=%22test%20file.txt%22 → "test file.txt")
try {
processedValue = decodeURIComponent(processedValue);
if (processedValue.startsWith('"') && processedValue.endsWith('"')) {
processedValue = processedValue.slice(1, -1);
}
} catch {
// Keep raw value
}
}
acc[key.toLowerCase().trim()] = decodedValue;
acc[key.toLowerCase().trim()] = processedValue;
return acc;
},
{} as Record<string, string>,