Skip to content

Commit c1248c9

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 0948693 commit c1248c9

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
@@ -3651,6 +3651,9 @@ Found'`.
36513651
<!-- YAML
36523652
added: v0.1.13
36533653
changes:
3654+
- version: REPLACEME
3655+
pr-url: https://github.com/nodejs/node/pull/61597
3656+
description: The `httpValidation` option is supported now.
36543657
- version: v24.12.0
36553658
pr-url: https://github.com/nodejs/node/pull/59778
36563659
description: Add optimizeEmptyRequests option.
@@ -3703,6 +3706,16 @@ changes:
37033706
`readableHighWaterMark` and `writableHighWaterMark`. This affects
37043707
`highWaterMark` property of both `IncomingMessage` and `ServerResponse`.
37053708
**Default:** See [`stream.getDefaultHighWaterMark()`][].
3709+
* `httpValidation` {string} Controls HTTP header value validation strictness
3710+
for incoming requests. Accepted values are:
3711+
* `'strict'`: Strictest validation; rejects any non-ASCII or control
3712+
characters in header values.
3713+
* `'relaxed'`: Allows a limited set of non-ASCII characters in header
3714+
values, aligning with the
3715+
[Fetch specification](https://fetch.spec.whatwg.org/).
3716+
* `'insecure'`: Disables all header value validation (equivalent to
3717+
`insecureHTTPParser:true`).
3718+
Cannot be used together with `insecureHTTPParser`. **Default:** `'strict'`.
37063719
* `insecureHTTPParser` {boolean} If set to `true`, it will use an HTTP parser
37073720
with leniency flags enabled. Using the insecure parser should be avoided.
37083721
See [`--insecure-http-parser`][] for more information.
@@ -3959,6 +3972,9 @@ This can be overridden for servers and client requests by passing the
39593972
<!-- YAML
39603973
added: v0.3.6
39613974
changes:
3975+
- version: REPLACEME
3976+
pr-url: https://github.com/nodejs/node/pull/61597
3977+
description: The `httpValidation` option is supported now.
39623978
- version:
39633979
- v16.7.0
39643980
- v14.18.0
@@ -4014,6 +4030,16 @@ changes:
40144030
request to. **Default:** `'localhost'`.
40154031
* `hostname` {string} Alias for `host`. To support [`url.parse()`][],
40164032
`hostname` will be used if both `host` and `hostname` are specified.
4033+
* `httpValidation` {string} Controls HTTP header value validation strictness
4034+
for outgoing requests. Accepted values are:
4035+
* `'strict'`: Strictest validation; rejects any non-ASCII or control
4036+
characters in header values.
4037+
* `'relaxed'`: Allows a limited set of non-ASCII characters in header
4038+
values, aligning with the
4039+
[Fetch specification](https://fetch.spec.whatwg.org/).
4040+
* `'insecure'`: Disables all header value validation (equivalent to
4041+
`insecureHTTPParser:true`).
4042+
Cannot be used together with `insecureHTTPParser`. **Default:** `'strict'`.
40174043
* `insecureHTTPParser` {boolean} If set to `true`, it will use an HTTP parser
40184044
with leniency flags enabled. Using the insecure parser should be avoided.
40194045
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(){
@@ -417,32 +445,33 @@ function _storeHeader(firstLine, headers) {
417445
trailer: false,
418446
header: firstLine,
419447
};
448+
constlenient=this._isLenientHeaderValidation();
420449

421450
if(headers){
422451
if(headers===this[kOutHeaders]){
423452
for(constkeyinheaders){
424453
constentry=headers[key];
425-
processHeader(this,state,entry[0],entry[1],false);
454+
processHeader(this,state,entry[0],entry[1],false,lenient);
426455
}
427456
}elseif(ArrayIsArray(headers)){
428457
if(headers.length&&ArrayIsArray(headers[0])){
429458
for(leti=0;i<headers.length;i++){
430459
constentry=headers[i];
431-
processHeader(this,state,entry[0],entry[1],true);
460+
processHeader(this,state,entry[0],entry[1],true,lenient);
432461
}
433462
}else{
434463
if(headers.length%2!==0){
435464
thrownewERR_INVALID_ARG_VALUE('headers',headers);
436465
}
437466

438467
for(letn=0;n<headers.length;n+=2){
439-
processHeader(this,state,headers[n+0],headers[n+1],true);
468+
processHeader(this,state,headers[n+0],headers[n+1],true,lenient);
440469
}
441470
}
442471
}else{
443472
for(constkeyinheaders){
444473
if(ObjectHasOwn(headers,key)){
445-
processHeader(this,state,key,headers[key],true);
474+
processHeader(this,state,key,headers[key],true,lenient);
446475
}
447476
}
448477
}
@@ -541,7 +570,7 @@ function _storeHeader(firstLine, headers) {
541570
if(state.expect)this._send('');
542571
}
543572

544-
functionprocessHeader(self,state,key,value,validate){
573+
functionprocessHeader(self,state,key,value,validate,lenient){
545574
if(validate)
546575
validateHeaderName(key);
547576

@@ -568,17 +597,17 @@ function processHeader(self, state, key, value, validate) {
568597
// Retain for(;;) loop for performance reasons
569598
// Refs: https://github.com/nodejs/node/pull/30958
570599
for(leti=0;i<value.length;i++)
571-
storeHeader(self,state,key,value[i],validate);
600+
storeHeader(self,state,key,value[i],validate,lenient);
572601
return;
573602
}
574603
value=value.join('; ');
575604
}
576-
storeHeader(self,state,key,value,validate);
605+
storeHeader(self,state,key,value,validate,lenient);
577606
}
578607

579-
functionstoreHeader(self,state,key,value,validate){
608+
functionstoreHeader(self,state,key,value,validate,lenient){
580609
if(validate)
581-
validateHeaderValue(key,value);
610+
validateHeaderValue(key,value,lenient);
582611
state.header+=key+': '+value+'\r\n';
583612
matchHeader(self,state,key,value);
584613
}
@@ -624,11 +653,11 @@ const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFra
624653
}
625654
}));
626655

627-
constvalidateHeaderValue=assignFunctionName('validateHeaderValue',hideStackFrames((name,value)=>{
656+
constvalidateHeaderValue=assignFunctionName('validateHeaderValue',hideStackFrames((name,value,lenient)=>{
628657
if(value===undefined){
629658
thrownewERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value,name);
630659
}
631-
if(checkInvalidHeaderChar(value)){
660+
if(checkInvalidHeaderChar(value,lenient)){
632661
debug('Header "%s" contains invalid characters',name);
633662
thrownewERR_INVALID_CHAR.HideStackFramesError('header content',name);
634663
}
@@ -653,7 +682,13 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
653682
thrownewERR_HTTP_HEADERS_SENT('set');
654683
}
655684
validateHeaderName(name);
656-
validateHeaderValue(name,value);
685+
if(value===undefined){
686+
thrownewERR_HTTP_INVALID_HEADER_VALUE(value,name);
687+
}
688+
if(checkInvalidHeaderChar(value,this._isLenientHeaderValidation())){
689+
debug('Header "%s" contains invalid characters',name);
690+
thrownewERR_INVALID_CHAR('header content',name);
691+
}
657692

658693
letheaders=this[kOutHeaders];
659694
if(headers===null)
@@ -711,7 +746,13 @@ OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) {
711746
thrownewERR_HTTP_HEADERS_SENT('append');
712747
}
713748
validateHeaderName(name);
714-
validateHeaderValue(name,value);
749+
if(value===undefined){
750+
thrownewERR_HTTP_INVALID_HEADER_VALUE(value,name);
751+
}
752+
if(checkInvalidHeaderChar(value,this._isLenientHeaderValidation())){
753+
debug('Header "%s" contains invalid characters',name);
754+
thrownewERR_INVALID_CHAR('header content',name);
755+
}
715756

716757
constfield=name.toLowerCase();
717758
constheaders=this[kOutHeaders];
@@ -1007,12 +1048,13 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10071048

10081049
// Check if the field must be sent several times
10091050
constisArrayValue=ArrayIsArray(value);
1051+
constlenient=this._isLenientHeaderValidation();
10101052
if(
10111053
isArrayValue&&value.length>1&&
10121054
(!this[kUniqueHeaders]||!this[kUniqueHeaders].has(field.toLowerCase()))
10131055
){
10141056
for(letj=0,l=value.length;j<l;j++){
1015-
if(checkInvalidHeaderChar(value[j])){
1057+
if(checkInvalidHeaderChar(value[j],lenient)){
10161058
debug('Trailer "%s"[%d] contains invalid characters',field,j);
10171059
thrownewERR_INVALID_CHAR('trailer content',field);
10181060
}
@@ -1023,7 +1065,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10231065
value=value.join('; ');
10241066
}
10251067

1026-
if(checkInvalidHeaderChar(value)){
1068+
if(checkInvalidHeaderChar(value,lenient)){
10271069
debug('Trailer "%s" contains invalid characters',field);
10281070
thrownewERR_INVALID_CHAR('trailer content',field);
10291071
}

0 commit comments

Comments
 (0)