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 <kakumanurajeshkumar@gmail.com> PR-URL: #61597 Refs: #61582 Refs: https://fetch.spec.whatwg.org/#header-value Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com>
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
constkError=Symbol('kError');
120122
constkPath=Symbol('kPath');
121123

122-
constkLenientAll=HTTPParser.kLenientAll|0;
123-
constkLenientNone=HTTPParser.kLenientNone|0;
124-
125124
constHTTP_CLIENT_TRACE_EVENT_NAME='http.client.request';
126125

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

300299
this.insecureHTTPParser=insecureHTTPParser;
301300

301+
consthttpValidation=options.httpValidation;
302+
if(httpValidation!==undefined){
303+
validateOneOf(httpValidation,'options.httpValidation',
304+
['strict','relaxed','insecure']);
305+
if(insecureHTTPParser!==undefined){
306+
thrownewERR_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
functiontickOnSocket(req,socket){
908922
constparser=parsers.alloc();
909923
req.socket=socket;
910-
constlenient=req.insecureHTTPParser===undefined ?
911-
isLenient() : req.insecureHTTPParser;
924+
constlenientFlags=calculateLenientFlags(req.httpValidation,req.insecureHTTPParser);
912925
parser.initialize(HTTPParser.RESPONSE,
913926
newHTTPClientAsyncResource('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
returntrue;
257257
}
258258

259-
constheaderCharRegex=/[^\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+
conststrictHeaderCharRegex=/[^\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+
constlenientHeaderCharRegex=/[\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-
functioncheckInvalidHeaderChar(val){
269-
returnheaderCharRegex.test(val);
281+
functioncheckInvalidHeaderChar(val,lenient=false){
282+
constregex=lenient ? lenientHeaderCharRegex : strictHeaderCharRegex;
283+
returnregex.test(val);
270284
}
271285

272286
functioncleanParser(parser){
@@ -300,6 +314,19 @@ function isLenient() {
300314
returninsecureHTTPParser;
301315
}
302316

317+
functioncalculateLenientFlags(httpValidation,insecureHTTPParserOption){
318+
if(httpValidation==='strict'){
319+
returnHTTPParser.kLenientNone|0;
320+
}elseif(httpValidation==='relaxed'){
321+
returnHTTPParser.kLenientHeaderValueRelaxed|0;
322+
}elseif(httpValidation==='insecure'){
323+
returnHTTPParser.kLenientAll|0;
324+
}
325+
constlenient=insecureHTTPParserOption===undefined ?
326+
isLenient() : insecureHTTPParserOption;
327+
returnlenient ? 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+
returnthis.httpValidation!=='strict';
170+
}
171+
// ServerResponse: check server's httpValidation option
172+
constserverHttpValidation=this.req?.socket?.server?.httpValidation;
173+
if(serverHttpValidation!==undefined){
174+
returnserverHttpValidation!=='strict';
175+
}
176+
// Legacy insecureHTTPParser - ClientRequest has it directly
177+
if(typeofthis.insecureHTTPParser==='boolean'){
178+
returnthis.insecureHTTPParser;
179+
}
180+
// ServerResponse can access via req.socket.server
181+
constserverOption=this.req?.socket?.server?.insecureHTTPParser;
182+
if(typeofserverOption==='boolean'){
183+
returnserverOption;
184+
}
185+
// Fall back to global option
186+
returnisLenient();
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+
constlenient=this._isLenientHeaderValidation();
414443

415444
if(headers){
416445
if(headers===this[kOutHeaders]){
417446
for(constkeyinheaders){
418447
constentry=headers[key];
419-
processHeader(this,state,entry[0],entry[1],false);
448+
processHeader(this,state,entry[0],entry[1],false,lenient);
420449
}
421450
}elseif(ArrayIsArray(headers)){
422451
if(headers.length&&ArrayIsArray(headers[0])){
423452
for(leti=0;i<headers.length;i++){
424453
constentry=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
thrownewERR_INVALID_ARG_VALUE('headers',headers);
430459
}
431460

432461
for(letn=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(constkeyinheaders){
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-
functionprocessHeader(self,state,key,value,validate){
567+
functionprocessHeader(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(leti=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-
functionstoreHeader(self,state,key,value,validate){
602+
functionstoreHeader(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-
constvalidateHeaderValue=assignFunctionName('validateHeaderValue',hideStackFrames((name,value)=>{
650+
constvalidateHeaderValue=assignFunctionName('validateHeaderValue',hideStackFrames((name,value,lenient)=>{
622651
if(value===undefined){
623652
thrownewERR_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
thrownewERR_INVALID_CHAR.HideStackFramesError('header content',name);
628657
}
@@ -647,7 +676,13 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
647676
thrownewERR_HTTP_HEADERS_SENT('set');
648677
}
649678
validateHeaderName(name);
650-
validateHeaderValue(name,value);
679+
if(value===undefined){
680+
thrownewERR_HTTP_INVALID_HEADER_VALUE(value,name);
681+
}
682+
if(checkInvalidHeaderChar(value,this._isLenientHeaderValidation())){
683+
debug('Header "%s" contains invalid characters',name);
684+
thrownewERR_INVALID_CHAR('header content',name);
685+
}
651686

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

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

10021043
// Check if the field must be sent several times
10031044
constisArrayValue=ArrayIsArray(value);
1045+
constlenient=this._isLenientHeaderValidation();
10041046
if(
10051047
isArrayValue&&value.length>1&&
10061048
(!this[kUniqueHeaders]||!this[kUniqueHeaders].has(field.toLowerCase()))
10071049
){
10081050
for(letj=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
thrownewERR_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
thrownewERR_INVALID_CHAR('trailer content',field);
10231065
}

0 commit comments

Comments
 (0)