Skip to content

Commit 2d6670e

Browse files
committed
http: support relaxed header validation via insecureHTTPParser
Add support for lenient outgoing header value validation when the insecureHTTPParser option is set. By default, strict validation per RFC 7230 is used (rejecting control characters except HTAB). When insecureHTTPParser is enabled, validation follows the Fetch spec (rejecting only NUL, CR, and LF). This applies to setHeader(), appendHeader(), and addTrailers() on OutgoingMessage (both ClientRequest and ServerResponse). Fixes: #61582
1 parent f77a709 commit 2d6670e

File tree

3 files changed

+117
-24
lines changed

3 files changed

+117
-24
lines changed

lib/_http_common.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,17 +256,31 @@ function checkIsHttpToken(val) {
256256
return true;
257257
}
258258

259-
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
259+
// Strict header value regex per RFC 7230 (original/default behavior):
260+
// field-value = *( field-content / obs-fold )
261+
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
262+
// field-vchar = VCHAR / obs-text
263+
// This rejects control characters (0x00-0x1f except HTAB) and DEL (0x7f).
264+
const strictHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
265+
266+
// Lenient header value regex per Fetch spec (https://fetch.spec.whatwg.org/#header-value):
267+
// - Must contain no 0x00 (NUL) or HTTP newline bytes (0x0a LF, 0x0d CR)
268+
// - Must be byte sequences (0x00-0xff), not arbitrary unicode
269+
// This allows most control characters except NUL, CR, and LF.
270+
// eslint-disable-next-line no-control-regex
271+
const lenientHeaderCharRegex = /[\x00\x0a\x0d]|[^\x00-\xff]/;
272+
260273
/**
261-
* True if val contains an invalid field-vchar
262-
* field-value = *( field-content / obs-fold )
263-
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
264-
* field-vchar = VCHAR / obs-text
274+
* True if val contains an invalid header value character.
275+
* By default uses strict validation per RFC 7230.
276+
* When lenient=true, uses relaxed validation per Fetch spec.
265277
* @param {string} val
278+
* @param {boolean} [lenient] - Use lenient validation (Fetch spec rules)
266279
* @returns {boolean}
267280
*/
268-
function checkInvalidHeaderChar(val) {
269-
return headerCharRegex.test(val);
281+
function checkInvalidHeaderChar(val, lenient = false) {
282+
const regex = lenient ? lenientHeaderCharRegex : strictHeaderCharRegex;
283+
return regex.test(val);
270284
}
271285

272286
function cleanParser(parser) {

lib/_http_outgoing.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const {
4444
_checkIsHttpToken: checkIsHttpToken,
4545
_checkInvalidHeaderChar: checkInvalidHeaderChar,
4646
chunkExpression: RE_TE_CHUNKED,
47+
isLenient,
4748
} = require('_http_common');
4849
const {
4950
defaultTriggerAsyncIdScope,
@@ -158,6 +159,24 @@ function OutgoingMessage(options) {
158159
ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
159160
ObjectSetPrototypeOf(OutgoingMessage, Stream);
160161

162+
// Check if lenient header validation should be used.
163+
// For ClientRequest: checks this.insecureHTTPParser
164+
// For ServerResponse: checks the server's insecureHTTPParser
165+
// Falls back to global --insecure-http-parser flag.
166+
OutgoingMessage.prototype._isLenientHeaderValidation = function() {
167+
// ClientRequest has insecureHTTPParser directly
168+
if (typeof this.insecureHTTPParser === 'boolean') {
169+
return this.insecureHTTPParser;
170+
}
171+
// ServerResponse can access via req.socket.server
172+
const serverOption = this.req?.socket?.server?.insecureHTTPParser;
173+
if (typeof serverOption === 'boolean') {
174+
return serverOption;
175+
}
176+
// Fall back to global option
177+
return isLenient();
178+
};
179+
161180
ObjectDefineProperty(OutgoingMessage.prototype, 'errored', {
162181
__proto__: null,
163182
get() {
@@ -642,7 +661,13 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
642661
throw new ERR_HTTP_HEADERS_SENT('set');
643662
}
644663
validateHeaderName(name);
645-
validateHeaderValue(name, value);
664+
if (value === undefined) {
665+
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
666+
}
667+
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
668+
debug('Header "%s" contains invalid characters', name);
669+
throw new ERR_INVALID_CHAR('header content', name);
670+
}
646671

647672
let headers = this[kOutHeaders];
648673
if (headers === null)
@@ -700,7 +725,13 @@ OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) {
700725
throw new ERR_HTTP_HEADERS_SENT('append');
701726
}
702727
validateHeaderName(name);
703-
validateHeaderValue(name, value);
728+
if (value === undefined) {
729+
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
730+
}
731+
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
732+
debug('Header "%s" contains invalid characters', name);
733+
throw new ERR_INVALID_CHAR('header content', name);
734+
}
704735

705736
const field = name.toLowerCase();
706737
const headers = this[kOutHeaders];
@@ -996,12 +1027,13 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
9961027

9971028
// Check if the field must be sent several times
9981029
const isArrayValue = ArrayIsArray(value);
1030+
const lenient = this._isLenientHeaderValidation();
9991031
if (
10001032
isArrayValue && value.length > 1 &&
10011033
(!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase()))
10021034
) {
10031035
for (let j = 0, l = value.length; j < l; j++) {
1004-
if (checkInvalidHeaderChar(value[j])) {
1036+
if (checkInvalidHeaderChar(value[j], lenient)) {
10051037
debug('Trailer "%s"[%d] contains invalid characters', field, j);
10061038
throw new ERR_INVALID_CHAR('trailer content', field);
10071039
}
@@ -1012,7 +1044,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10121044
value = value.join('; ');
10131045
}
10141046

1015-
if (checkInvalidHeaderChar(value)) {
1047+
if (checkInvalidHeaderChar(value, lenient)) {
10161048
debug('Trailer "%s" contains invalid characters', field);
10171049
throw new ERR_INVALID_CHAR('trailer content', field);
10181050
}

test/parallel/test-http-invalidheaderfield2.js

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,30 +59,77 @@ const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('_http_common');
5959
});
6060

6161

62-
// Good header field values
62+
// ============================================================================
63+
// Strict header value validation (default) - per RFC 7230
64+
// Rejects control characters (0x00-0x1f except HTAB) and DEL (0x7f)
65+
// ============================================================================
66+
67+
// Good header field values in strict mode
6368
[
6469
'foo bar',
65-
'foo\tbar',
70+
'foo\tbar', // HTAB is allowed
6671
'0123456789ABCdef',
6772
'!@#$%^&*()-_=+\\;\':"[]{}<>,./?|~`',
73+
'\x80\x81\xff', // obs-text (0x80-0xff) is allowed
6874
].forEach(function(str) {
6975
assert.strictEqual(
7076
_checkInvalidHeaderChar(str), false,
71-
`_checkInvalidHeaderChar(${inspect(str)}) unexpectedly failed`);
77+
`_checkInvalidHeaderChar(${inspect(str)}) unexpectedly failed in strict mode`);
7278
});
7379

74-
// Bad header field values
80+
// Bad header field values in strict mode
81+
// Control characters (except HTAB) and DEL are rejected
7582
[
76-
'foo\rbar',
77-
'foo\nbar',
78-
'foo\r\nbar',
79-
'中文呢', // unicode
80-
'\x7FMe!',
81-
'Testing 123\x00',
82-
'foo\vbar',
83-
'Ding!\x07',
83+
'foo\x00bar', // NUL
84+
'foo\x01bar', // SOH
85+
'foo\rbar', // CR
86+
'foo\nbar', // LF
87+
'foo\r\nbar', // CRLF
88+
'foo\x7Fbar', // DEL
89+
'中文呢', // unicode > 0xff
8490
].forEach(function(str) {
8591
assert.strictEqual(
8692
_checkInvalidHeaderChar(str), true,
87-
`_checkInvalidHeaderChar(${inspect(str)}) unexpectedly succeeded`);
93+
`_checkInvalidHeaderChar(${inspect(str)}) unexpectedly succeeded in strict mode`);
94+
});
95+
96+
97+
// ============================================================================
98+
// Lenient header value validation (with insecureHTTPParser) - per Fetch spec
99+
// Only NUL (0x00), CR (0x0d), LF (0x0a), and chars > 0xff are rejected
100+
// ============================================================================
101+
102+
// Good header field values in lenient mode
103+
// CTL characters (except NUL, LF, CR) are valid per Fetch spec
104+
[
105+
'foo bar',
106+
'foo\tbar',
107+
'0123456789ABCdef',
108+
'!@#$%^&*()-_=+\\;\':"[]{}<>,./?|~`',
109+
'\x01\x02\x03\x04\x05\x06\x07\x08', // 0x01-0x08
110+
'foo\x0bbar', // VT (0x0b)
111+
'foo\x0cbar', // FF (0x0c)
112+
'\x0e\x0f\x10\x11\x12\x13\x14\x15', // 0x0e-0x15
113+
'\x16\x17\x18\x19\x1a\x1b\x1c\x1d', // 0x16-0x1d
114+
'\x1e\x1f', // 0x1e-0x1f
115+
'\x7FMe!', // DEL (0x7f)
116+
'\x80\x81\xff', // obs-text (0x80-0xff)
117+
].forEach(function(str) {
118+
assert.strictEqual(
119+
_checkInvalidHeaderChar(str, true), false,
120+
`_checkInvalidHeaderChar(${inspect(str)}, true) unexpectedly failed in lenient mode`);
121+
});
122+
123+
// Bad header field values in lenient mode
124+
// Only NUL (0x00), LF (0x0a), CR (0x0d), and characters > 0xff are invalid
125+
[
126+
'foo\rbar', // CR (0x0d)
127+
'foo\nbar', // LF (0x0a)
128+
'foo\r\nbar', // CRLF
129+
'中文呢', // unicode > 0xff
130+
'Testing 123\x00', // NUL (0x00)
131+
].forEach(function(str) {
132+
assert.strictEqual(
133+
_checkInvalidHeaderChar(str, true), true,
134+
`_checkInvalidHeaderChar(${inspect(str)}, true) unexpectedly succeeded in lenient mode`);
88135
});

0 commit comments

Comments
 (0)