Skip to content

Commit 8ded765

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 8ded765

File tree

3 files changed

+116
-24
lines changed

3 files changed

+116
-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: 35 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,23 @@ 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 (this.insecureHTTPParser !== undefined) {
169+
return this.insecureHTTPParser;
170+
}
171+
// ServerResponse can access via req.socket.server
172+
if (this.req?.socket?.server?.insecureHTTPParser) {
173+
return this.req.socket.server.insecureHTTPParser;
174+
}
175+
// Fall back to global option
176+
return isLenient();
177+
};
178+
161179
ObjectDefineProperty(OutgoingMessage.prototype, 'errored', {
162180
__proto__: null,
163181
get() {
@@ -642,7 +660,13 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
642660
throw new ERR_HTTP_HEADERS_SENT('set');
643661
}
644662
validateHeaderName(name);
645-
validateHeaderValue(name, value);
663+
if (value === undefined) {
664+
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
665+
}
666+
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
667+
debug('Header "%s" contains invalid characters', name);
668+
throw new ERR_INVALID_CHAR('header content', name);
669+
}
646670

647671
let headers = this[kOutHeaders];
648672
if (headers === null)
@@ -700,7 +724,13 @@ OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) {
700724
throw new ERR_HTTP_HEADERS_SENT('append');
701725
}
702726
validateHeaderName(name);
703-
validateHeaderValue(name, value);
727+
if (value === undefined) {
728+
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
729+
}
730+
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
731+
debug('Header "%s" contains invalid characters', name);
732+
throw new ERR_INVALID_CHAR('header content', name);
733+
}
704734

705735
const field = name.toLowerCase();
706736
const headers = this[kOutHeaders];
@@ -996,12 +1026,13 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
9961026

9971027
// Check if the field must be sent several times
9981028
const isArrayValue = ArrayIsArray(value);
1029+
const lenient = this._isLenientHeaderValidation();
9991030
if (
10001031
isArrayValue && value.length > 1 &&
10011032
(!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase()))
10021033
) {
10031034
for (let j = 0, l = value.length; j < l; j++) {
1004-
if (checkInvalidHeaderChar(value[j])) {
1035+
if (checkInvalidHeaderChar(value[j], lenient)) {
10051036
debug('Trailer "%s"[%d] contains invalid characters', field, j);
10061037
throw new ERR_INVALID_CHAR('trailer content', field);
10071038
}
@@ -1012,7 +1043,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10121043
value = value.join('; ');
10131044
}
10141045

1015-
if (checkInvalidHeaderChar(value)) {
1046+
if (checkInvalidHeaderChar(value, lenient)) {
10161047
debug('Trailer "%s" contains invalid characters', field);
10171048
throw new ERR_INVALID_CHAR('trailer content', field);
10181049
}

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)