@@ -103,7 +103,8 @@ export function sanitizeRequestHeaders(
103103
104104 for ( const [ key , value ] of request . headers ) {
105105 const lowerKey = key . toLowerCase ( ) ;
106- if ( lowerKey . startsWith ( 'x-forwarded-' ) && ! isProxyHeaderAllowed ( lowerKey , trustProxyHeaders ) ) {
106+ const isProxyHeader = lowerKey === 'forwarded' || lowerKey . startsWith ( 'x-forwarded-' ) ;
107+ if ( isProxyHeader && ! isProxyHeaderAllowed ( lowerKey , trustProxyHeaders ) ) {
107108 // eslint-disable-next-line no-console
108109 console . warn (
109110 `Received "${ key } " header but "trustProxyHeaders" was not set up to allow it.\n` +
@@ -199,6 +200,17 @@ function validateHeaders(
199200 }
200201 }
201202
203+ const forwarded = headers . get ( 'forwarded' ) ;
204+ if ( forwarded ) {
205+ const forwardedParams = parseForwardedHeader ( forwarded ) ;
206+ if ( forwardedParams . host && ! disableHostCheck ) {
207+ verifyHostAllowed ( 'Forwarded "host"' , forwardedParams . host , allowedHosts ) ;
208+ }
209+ if ( forwardedParams . proto && ! VALID_PROTO_REGEX . test ( forwardedParams . proto ) ) {
210+ throw new Error ( 'Header "forwarded" proto parameter must be either "http" or "https".' ) ;
211+ }
212+ }
213+
202214 const xForwardedPort = getFirstHeaderValue ( headers . get ( 'x-forwarded-port' ) ) ;
203215 if ( xForwardedPort && ! VALID_PORT_REGEX . test ( xForwardedPort ) ) {
204216 throw new Error ( 'Header "x-forwarded-port" must be a numeric value.' ) ;
@@ -251,12 +263,158 @@ export function normalizeTrustProxyHeaders(
251263 return new Set ( [ TRUST_ALL_PROXY_HEADERS ] ) ;
252264 }
253265
254- const normalizedTrustedProxyHeaders = new Set ( trustProxyHeaders . map ( ( h ) => h . toLowerCase ( ) ) ) ;
255- if ( normalizedTrustedProxyHeaders . has ( TRUST_ALL_PROXY_HEADERS ) ) {
256- throw new Error (
257- `"${ TRUST_ALL_PROXY_HEADERS } " is not allowed as a value for the "trustProxyHeaders" option.` ,
258- ) ;
266+ const normalizedTrustedProxyHeaders = new Set < string > ( ) ;
267+ for ( const header of trustProxyHeaders ) {
268+ const lowerHeader = header . toLowerCase ( ) ;
269+ if ( lowerHeader === TRUST_ALL_PROXY_HEADERS ) {
270+ throw new Error (
271+ `"${ TRUST_ALL_PROXY_HEADERS } " is not allowed as a value for the "trustProxyHeaders" option.` ,
272+ ) ;
273+ }
274+ const isValid = lowerHeader === 'forwarded' || lowerHeader . startsWith ( 'x-forwarded-' ) ;
275+ if ( ! isValid ) {
276+ throw new Error (
277+ `"${ header } " is not a valid proxy header. Trusted proxy headers must be "forwarded" or start with "x-forwarded-".` ,
278+ ) ;
279+ }
280+ normalizedTrustedProxyHeaders . add ( lowerHeader ) ;
259281 }
260282
261283 return normalizedTrustedProxyHeaders ;
262284}
285+
286+ /**
287+ * Parses the standard `Forwarded` header (RFC 7239).
288+ * It extracts the parameters from the first (leftmost) element in the header.
289+ *
290+ * @param headerValue - The value of the `Forwarded` header.
291+ * @returns A record of lowercase parameter names to their values.
292+ */
293+ export function parseForwardedHeader (
294+ headerValue : string | null | undefined ,
295+ ) : Record < string , string > {
296+ if ( ! headerValue ) {
297+ return { } ;
298+ }
299+
300+ const params : Record < string , string > = { } ;
301+ let inQuotes = false ;
302+ let escaped = false ;
303+ let currentKey = '' ;
304+ let currentValue = '' ;
305+ let isParsingValue = false ;
306+ let isKeyEnded = false ;
307+ let isParsingValueEnded = false ;
308+
309+ for ( const char of headerValue ) {
310+ if ( escaped ) {
311+ escaped = false ;
312+ if ( isParsingValue ) {
313+ currentValue += char ;
314+ } else {
315+ currentKey += char ;
316+ }
317+ continue ;
318+ }
319+
320+ if ( char === '\\' ) {
321+ if ( inQuotes ) {
322+ escaped = true ;
323+ } else if ( isParsingValue ) {
324+ currentValue += char ;
325+ } else {
326+ currentKey += char ;
327+ }
328+ continue ;
329+ }
330+
331+ if ( char === '"' ) {
332+ inQuotes = ! inQuotes ;
333+ continue ;
334+ }
335+
336+ if ( inQuotes ) {
337+ if ( isParsingValue ) {
338+ currentValue += char ;
339+ } else {
340+ currentKey += char ;
341+ }
342+ continue ;
343+ }
344+
345+ if ( char === ',' ) {
346+ addParam ( currentKey , currentValue , isParsingValue , params ) ;
347+ break ;
348+ }
349+
350+ if ( char === ';' ) {
351+ addParam ( currentKey , currentValue , isParsingValue , params ) ;
352+ currentKey = '' ;
353+ currentValue = '' ;
354+ isParsingValue = false ;
355+ isKeyEnded = false ;
356+ isParsingValueEnded = false ;
357+ continue ;
358+ }
359+
360+ if ( char === '=' ) {
361+ if ( ! isParsingValue ) {
362+ isParsingValue = true ;
363+ } else {
364+ currentValue += char ;
365+ }
366+ continue ;
367+ }
368+
369+ if ( char === ' ' || char === '\t' ) {
370+ if ( isParsingValue ) {
371+ if ( currentValue . length > 0 ) {
372+ isParsingValueEnded = true ;
373+ }
374+ } else if ( currentKey . length > 0 ) {
375+ isKeyEnded = true ;
376+ }
377+ continue ;
378+ }
379+
380+ if ( isParsingValue ) {
381+ if ( ! isParsingValueEnded ) {
382+ currentValue += char ;
383+ }
384+ } else if ( isKeyEnded ) {
385+ currentKey = char ;
386+ isKeyEnded = false ;
387+ } else {
388+ currentKey += char ;
389+ }
390+ }
391+
392+ if ( currentKey || currentValue || isParsingValue ) {
393+ addParam ( currentKey , currentValue , isParsingValue , params ) ;
394+ }
395+
396+ return params ;
397+ }
398+
399+ /**
400+ * Helper function to add a parameter to the params object.
401+ * @param key - The key to add.
402+ * @param value - The value to add.
403+ * @param hasValue - Whether the parameter has a value.
404+ * @param params - The params object to add the parameter to.
405+ */
406+ function addParam (
407+ key : string ,
408+ value : string ,
409+ hasValue : boolean ,
410+ params : Record < string , string > ,
411+ ) : void {
412+ if ( ! hasValue ) {
413+ return ;
414+ }
415+
416+ const trimmedKey = key . trim ( ) . toLowerCase ( ) ;
417+ if ( trimmedKey ) {
418+ params [ trimmedKey ] = value ;
419+ }
420+ }
0 commit comments