Skip to content

Commit 49462ec

Browse files
RajeshKumar11aduh95
authored andcommitted
http: add httpValidation option to configure header value validation
Add a new httpValidation option to http.createServer() and http.request() / http.ClientRequest that controls how strictly HTTP header values are validated: - 'strict' - reject any non-ASCII or control characters (default) - 'relaxed' - allow the non-ASCII characters permitted by the Fetch specification (kLenientHeaderValueRelaxed) - 'insecure' - disable all validation (like insecureHTTPParser) The option is threaded through _storeHeader -> processHeader -> storeHeader -> validateHeaderValue, and also through writeInformation -> processInformationHeader -> validateHeaderValue. Cannot be used together with insecureHTTPParser. Fixes: #61582 Signed-off-by: RajeshKumar11 <[email protected]> PR-URL: #61597 Refs: #61582 Refs: https://fetch.spec.whatwg.org/#header-value Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Tim Perry <[email protected]>
1 parent 4da442f commit 49462ec

8 files changed

Lines changed: 668 additions & 53 deletions

File tree

doc/api/http.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,6 +3662,9 @@ Found'`.
36623662
<!-- YAML
36633663
added: v0.1.13
36643664
changes:
3665+
- version: REPLACEME
3666+
pr-url: https://github.com/nodejs/node/pull/61597
3667+
description: The `httpValidation` option is supported now.
36653668
- version:
36663669
- v25.1.0
36673670
- v24.12.0
@@ -3718,6 +3721,16 @@ changes:
37183721
`readableHighWaterMark` and `writableHighWaterMark`. This affects
37193722
`highWaterMark` property of both `IncomingMessage` and `ServerResponse`.
37203723
**Default:** See [`stream.getDefaultHighWaterMark()`][].
3724+
* `httpValidation` {string} Controls HTTP header value validation strictness
3725+
for incoming requests. Accepted values are:
3726+
* `'strict'`: Strictest validation; rejects any non-ASCII or control
3727+
characters in header values.
3728+
* `'relaxed'`: Allows a limited set of non-ASCII characters in header
3729+
values, aligning with the
3730+
[Fetch specification](https://fetch.spec.whatwg.org/).
3731+
* `'insecure'`: Disables all header value validation (equivalent to
3732+
`insecureHTTPParser: true`).
3733+
Cannot be used together with `insecureHTTPParser`. **Default:** `'strict'`.
37213734
* `insecureHTTPParser` {boolean} If set to `true`, it will use an HTTP parser
37223735
with leniency flags enabled. Using the insecure parser should be avoided.
37233736
See [`--insecure-http-parser`][] for more information.
@@ -3974,6 +3987,9 @@ This can be overridden for servers and client requests by passing the
39743987
<!-- YAML
39753988
added: v0.3.6
39763989
changes:
3990+
- version: REPLACEME
3991+
pr-url: https://github.com/nodejs/node/pull/61597
3992+
description: The `httpValidation` option is supported now.
39773993
- version:
39783994
- v16.7.0
39793995
- v14.18.0
@@ -4029,6 +4045,16 @@ changes:
40294045
request to. **Default:** `'localhost'`.
40304046
* `hostname` {string} Alias for `host`. To support [`url.parse()`][],
40314047
`hostname` will be used if both `host` and `hostname` are specified.
4048+
* `httpValidation` {string} Controls HTTP header value validation strictness
4049+
for outgoing requests. Accepted values are:
4050+
* `'strict'`: Strictest validation; rejects any non-ASCII or control
4051+
characters in header values.
4052+
* `'relaxed'`: Allows a limited set of non-ASCII characters in header
4053+
values, aligning with the
4054+
[Fetch specification](https://fetch.spec.whatwg.org/).
4055+
* `'insecure'`: Disables all header value validation (equivalent to
4056+
`insecureHTTPParser: true`).
4057+
Cannot be used together with `insecureHTTPParser`. **Default:** `'strict'`.
40324058
* `insecureHTTPParser` {boolean} If set to `true`, it will use an HTTP parser
40334059
with leniency flags enabled. Using the insecure parser should be avoided.
40344060
See [`--insecure-http-parser`][] for more information.

lib/_http_client.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const {
4646
freeParser,
4747
parsers,
4848
HTTPParser,
49-
isLenient,
49+
calculateLenientFlags,
5050
prepareError,
5151
kSkipPendingData,
5252
} = require('_http_common');
@@ -74,6 +74,7 @@ const {
7474
codes: {
7575
ERR_HTTP_HEADERS_SENT,
7676
ERR_INVALID_ARG_TYPE,
77+
ERR_INVALID_ARG_VALUE,
7778
ERR_INVALID_HTTP_TOKEN,
7879
ERR_INVALID_PROTOCOL,
7980
ERR_UNESCAPED_CHARACTERS,
@@ -82,6 +83,7 @@ const {
8283
const {
8384
validateInteger,
8485
validateBoolean,
86+
validateOneOf,
8587
validateString,
8688
} = require('internal/validators');
8789
const { getTimerDuration } = require('internal/timers');
@@ -119,9 +121,6 @@ const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
119121
const kError = Symbol('kError');
120122
const kPath = Symbol('kPath');
121123

122-
const kLenientAll = HTTPParser.kLenientAll | 0;
123-
const kLenientNone = HTTPParser.kLenientNone | 0;
124-
125124
const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request';
126125

127126
function validateHost(host, name) {
@@ -299,6 +298,21 @@ function ClientRequest(input, options, cb) {
299298

300299
this.insecureHTTPParser = insecureHTTPParser;
301300

301+
const httpValidation = options.httpValidation;
302+
if (httpValidation !== undefined) {
303+
validateOneOf(httpValidation, 'options.httpValidation',
304+
['strict', 'relaxed', 'insecure']);
305+
if (insecureHTTPParser !== undefined) {
306+
throw new ERR_INVALID_ARG_VALUE(
307+
'options.httpValidation',
308+
httpValidation,
309+
'cannot be used together with options.insecureHTTPParser',
310+
);
311+
}
312+
}
313+
314+
this.httpValidation = httpValidation;
315+
302316
if (options.joinDuplicateHeaders !== undefined) {
303317
validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders');
304318
}
@@ -907,12 +921,11 @@ function emitFreeNT(req) {
907921
function tickOnSocket(req, socket) {
908922
const parser = parsers.alloc();
909923
req.socket = socket;
910-
const lenient = req.insecureHTTPParser === undefined ?
911-
isLenient() : req.insecureHTTPParser;
924+
const lenientFlags = calculateLenientFlags(req.httpValidation, req.insecureHTTPParser);
912925
parser.initialize(HTTPParser.RESPONSE,
913926
new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req),
914927
req.maxHeaderSize || 0,
915-
lenient ? kLenientAll : kLenientNone);
928+
lenientFlags);
916929
parser.socket = socket;
917930
parser.outgoing = req;
918931
req.parser = parser;

lib/_http_common.js

Lines changed: 35 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) {
@@ -300,6 +314,19 @@ function isLenient() {
300314
return insecureHTTPParser;
301315
}
302316

317+
function calculateLenientFlags(httpValidation, insecureHTTPParserOption) {
318+
if (httpValidation === 'strict') {
319+
return HTTPParser.kLenientNone | 0;
320+
} else if (httpValidation === 'relaxed') {
321+
return HTTPParser.kLenientHeaderValueRelaxed | 0;
322+
} else if (httpValidation === 'insecure') {
323+
return HTTPParser.kLenientAll | 0;
324+
}
325+
const lenient = insecureHTTPParserOption === undefined ?
326+
isLenient() : insecureHTTPParserOption;
327+
return lenient ? HTTPParser.kLenientAll | 0 : HTTPParser.kLenientNone | 0;
328+
}
329+
303330
module.exports = {
304331
_checkInvalidHeaderChar: checkInvalidHeaderChar,
305332
_checkIsHttpToken: checkIsHttpToken,
@@ -312,6 +339,7 @@ module.exports = {
312339
kIncomingMessage,
313340
HTTPParser,
314341
isLenient,
342+
calculateLenientFlags,
315343
prepareError,
316344
kSkipPendingData,
317345
};

lib/_http_outgoing.js

Lines changed: 57 additions & 15 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,33 @@ 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.httpValidation or this.insecureHTTPParser
164+
// For ServerResponse: checks the server's httpValidation or insecureHTTPParser
165+
// Falls back to global --insecure-http-parser flag.
166+
OutgoingMessage.prototype._isLenientHeaderValidation = function() {
167+
// New httpValidation option takes priority (ClientRequest case)
168+
if (this.httpValidation !== undefined) {
169+
return this.httpValidation !== 'strict';
170+
}
171+
// ServerResponse: check server's httpValidation option
172+
const serverHttpValidation = this.req?.socket?.server?.httpValidation;
173+
if (serverHttpValidation !== undefined) {
174+
return serverHttpValidation !== 'strict';
175+
}
176+
// Legacy insecureHTTPParser - ClientRequest has it directly
177+
if (typeof this.insecureHTTPParser === 'boolean') {
178+
return this.insecureHTTPParser;
179+
}
180+
// ServerResponse can access via req.socket.server
181+
const serverOption = this.req?.socket?.server?.insecureHTTPParser;
182+
if (typeof serverOption === 'boolean') {
183+
return serverOption;
184+
}
185+
// Fall back to global option
186+
return isLenient();
187+
};
188+
161189
ObjectDefineProperty(OutgoingMessage.prototype, 'errored', {
162190
__proto__: null,
163191
get() {
@@ -411,32 +439,33 @@ function _storeHeader(firstLine, headers) {
411439
trailer: false,
412440
header: firstLine,
413441
};
442+
const lenient = this._isLenientHeaderValidation();
414443

415444
if (headers) {
416445
if (headers === this[kOutHeaders]) {
417446
for (const key in headers) {
418447
const entry = headers[key];
419-
processHeader(this, state, entry[0], entry[1], false);
448+
processHeader(this, state, entry[0], entry[1], false, lenient);
420449
}
421450
} else if (ArrayIsArray(headers)) {
422451
if (headers.length && ArrayIsArray(headers[0])) {
423452
for (let i = 0; i < headers.length; i++) {
424453
const entry = headers[i];
425-
processHeader(this, state, entry[0], entry[1], true);
454+
processHeader(this, state, entry[0], entry[1], true, lenient);
426455
}
427456
} else {
428457
if (headers.length % 2 !== 0) {
429458
throw new ERR_INVALID_ARG_VALUE('headers', headers);
430459
}
431460

432461
for (let n = 0; n < headers.length; n += 2) {
433-
processHeader(this, state, headers[n + 0], headers[n + 1], true);
462+
processHeader(this, state, headers[n + 0], headers[n + 1], true, lenient);
434463
}
435464
}
436465
} else {
437466
for (const key in headers) {
438467
if (ObjectHasOwn(headers, key)) {
439-
processHeader(this, state, key, headers[key], true);
468+
processHeader(this, state, key, headers[key], true, lenient);
440469
}
441470
}
442471
}
@@ -535,7 +564,7 @@ function _storeHeader(firstLine, headers) {
535564
if (state.expect) this._send('');
536565
}
537566

538-
function processHeader(self, state, key, value, validate) {
567+
function processHeader(self, state, key, value, validate, lenient) {
539568
if (validate)
540569
validateHeaderName(key);
541570

@@ -562,17 +591,17 @@ function processHeader(self, state, key, value, validate) {
562591
// Retain for(;;) loop for performance reasons
563592
// Refs: https://github.com/nodejs/node/pull/30958
564593
for (let i = 0; i < value.length; i++)
565-
storeHeader(self, state, key, value[i], validate);
594+
storeHeader(self, state, key, value[i], validate, lenient);
566595
return;
567596
}
568597
value = value.join('; ');
569598
}
570-
storeHeader(self, state, key, value, validate);
599+
storeHeader(self, state, key, value, validate, lenient);
571600
}
572601

573-
function storeHeader(self, state, key, value, validate) {
602+
function storeHeader(self, state, key, value, validate, lenient) {
574603
if (validate)
575-
validateHeaderValue(key, value);
604+
validateHeaderValue(key, value, lenient);
576605
state.header += key + ': ' + value + '\r\n';
577606
matchHeader(self, state, key, value);
578607
}
@@ -618,11 +647,11 @@ const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFra
618647
}
619648
}));
620649

621-
const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value) => {
650+
const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value, lenient) => {
622651
if (value === undefined) {
623652
throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
624653
}
625-
if (checkInvalidHeaderChar(value)) {
654+
if (checkInvalidHeaderChar(value, lenient)) {
626655
debug('Header "%s" contains invalid characters', name);
627656
throw new ERR_INVALID_CHAR.HideStackFramesError('header content', name);
628657
}
@@ -647,7 +676,13 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
647676
throw new ERR_HTTP_HEADERS_SENT('set');
648677
}
649678
validateHeaderName(name);
650-
validateHeaderValue(name, value);
679+
if (value === undefined) {
680+
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
681+
}
682+
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
683+
debug('Header "%s" contains invalid characters', name);
684+
throw new ERR_INVALID_CHAR('header content', name);
685+
}
651686

652687
let headers = this[kOutHeaders];
653688
if (headers === null)
@@ -705,7 +740,13 @@ OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) {
705740
throw new ERR_HTTP_HEADERS_SENT('append');
706741
}
707742
validateHeaderName(name);
708-
validateHeaderValue(name, value);
743+
if (value === undefined) {
744+
throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name);
745+
}
746+
if (checkInvalidHeaderChar(value, this._isLenientHeaderValidation())) {
747+
debug('Header "%s" contains invalid characters', name);
748+
throw new ERR_INVALID_CHAR('header content', name);
749+
}
709750

710751
const field = name.toLowerCase();
711752
const headers = this[kOutHeaders];
@@ -1001,12 +1042,13 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10011042

10021043
// Check if the field must be sent several times
10031044
const isArrayValue = ArrayIsArray(value);
1045+
const lenient = this._isLenientHeaderValidation();
10041046
if (
10051047
isArrayValue && value.length > 1 &&
10061048
(!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase()))
10071049
) {
10081050
for (let j = 0, l = value.length; j < l; j++) {
1009-
if (checkInvalidHeaderChar(value[j])) {
1051+
if (checkInvalidHeaderChar(value[j], lenient)) {
10101052
debug('Trailer "%s"[%d] contains invalid characters', field, j);
10111053
throw new ERR_INVALID_CHAR('trailer content', field);
10121054
}
@@ -1017,7 +1059,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10171059
value = value.join('; ');
10181060
}
10191061

1020-
if (checkInvalidHeaderChar(value)) {
1062+
if (checkInvalidHeaderChar(value, lenient)) {
10211063
debug('Trailer "%s" contains invalid characters', field);
10221064
throw new ERR_INVALID_CHAR('trailer content', field);
10231065
}

0 commit comments

Comments
 (0)