Skip to content

Commit 41ebd2a

Browse files
committed
http: support relaxed header validation via insecureHTTPParser
Add support for relaxed HTTP header value validation when using the `insecureHTTPParser` option. This extends the existing option (which already relaxes inbound HTTP parsing) to also relax outbound header value validation. By default, header values are validated strictly per RFC 7230/9110, rejecting control characters (0x00-0x1f except HTAB) and DEL (0x7f). When `insecureHTTPParser: true` is set on a request/response, or `--insecure-http-parser` flag is used globally, header values are validated per Fetch spec rules, only rejecting NUL (0x00), CR (0x0d), LF (0x0a), and characters > 0xff. This allows Node.js to interoperate with servers/clients that use control characters in header values while maintaining security by always rejecting CR/LF (response splitting) and NUL characters. Refs: #61582 Refs: https://fetch.spec.whatwg.org/#header-value
1 parent f77a709 commit 41ebd2a

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/9110:
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/9110.
276+
* When lenient=true, uses relaxed validation per Fetch spec.
265277
* @param {string} val
278+
* @param {boolean} [lenient=false] - 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 !== undefined) {
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/9110
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)