@@ -17,7 +17,13 @@ import { getFirstHeaderValue } from '../../src/utils/validation';
1717 * as they are not allowed to be set directly using the `Node.js` Undici API or
1818 * the web `Headers` API.
1919 */
20- const HTTP2_PSEUDO_HEADERS = new Set ( [ ':method' , ':scheme' , ':authority' , ':path' , ':status' ] ) ;
20+ const HTTP2_PSEUDO_HEADERS : ReadonlySet < string > = new Set ( [
21+ ':method' ,
22+ ':scheme' ,
23+ ':authority' ,
24+ ':path' ,
25+ ':status' ,
26+ ] ) ;
2127
2228/**
2329 * Converts a Node.js `IncomingMessage` or `Http2ServerRequest` into a
@@ -27,18 +33,31 @@ const HTTP2_PSEUDO_HEADERS = new Set([':method', ':scheme', ':authority', ':path
2733 * be used by web platform APIs.
2834 *
2935 * @param nodeRequest - The Node.js request object (`IncomingMessage` or `Http2ServerRequest`) to convert.
36+ * @param trustProxyHeaders - A boolean or an array of allowed proxy headers.
37+ *
38+ * @remarks
39+ * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and
40+ * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure
41+ * level (e.g., at the reverse proxy or API gateway) before reaching the application.
42+ *
3043 * @returns A Web Standard `Request` object.
3144 */
3245export function createWebRequestFromNodeRequest (
3346 nodeRequest : IncomingMessage | Http2ServerRequest ,
47+ trustProxyHeaders ?: boolean | readonly string [ ] ,
3448) : Request {
49+ const trustProxyHeadersNormalized =
50+ trustProxyHeaders && typeof trustProxyHeaders !== 'boolean'
51+ ? new Set ( trustProxyHeaders . map ( ( h ) => h . toLowerCase ( ) ) )
52+ : trustProxyHeaders ;
53+
3554 const { headers, method = 'GET' } = nodeRequest ;
3655 const withBody = method !== 'GET' && method !== 'HEAD' ;
3756 const referrer = headers . referer && URL . canParse ( headers . referer ) ? headers . referer : undefined ;
3857
39- return new Request ( createRequestUrl ( nodeRequest ) , {
58+ return new Request ( createRequestUrl ( nodeRequest , trustProxyHeadersNormalized ) , {
4059 method,
41- headers : createRequestHeaders ( headers ) ,
60+ headers : createRequestHeaders ( headers , trustProxyHeadersNormalized ) ,
4261 body : withBody ? nodeRequest : undefined ,
4362 duplex : withBody ? 'half' : undefined ,
4463 referrer,
@@ -49,16 +68,27 @@ export function createWebRequestFromNodeRequest(
4968 * Creates a `Headers` object from Node.js `IncomingHttpHeaders`.
5069 *
5170 * @param nodeHeaders - The Node.js `IncomingHttpHeaders` object to convert.
71+ * @param trustProxyHeaders - A boolean or a set of allowed proxy headers.
5272 * @returns A `Headers` object containing the converted headers.
5373 */
54- function createRequestHeaders ( nodeHeaders : IncomingHttpHeaders ) : Headers {
74+ function createRequestHeaders (
75+ nodeHeaders : IncomingHttpHeaders ,
76+ trustProxyHeaders : boolean | ReadonlySet < string > | undefined ,
77+ ) : Headers {
5578 const headers = new Headers ( ) ;
5679
5780 for ( const [ name , value ] of Object . entries ( nodeHeaders ) ) {
5881 if ( HTTP2_PSEUDO_HEADERS . has ( name ) ) {
5982 continue ;
6083 }
6184
85+ if (
86+ name . toLowerCase ( ) . startsWith ( 'x-forwarded-' ) &&
87+ ! isProxyHeaderAllowed ( name . toLowerCase ( ) , trustProxyHeaders )
88+ ) {
89+ continue ;
90+ }
91+
6292 if ( typeof value === 'string' ) {
6393 headers . append ( name , value ) ;
6494 } else if ( Array . isArray ( value ) ) {
@@ -75,32 +105,92 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
75105 * Creates a `URL` object from a Node.js `IncomingMessage`, taking into account the protocol, host, and port.
76106 *
77107 * @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from.
108+ * @param trustProxyHeaders - A boolean or a set of allowed proxy headers.
109+ *
110+ * @remarks
111+ * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and
112+ * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure
113+ * level (e.g., at the reverse proxy or API gateway) before reaching the application.
114+ *
78115 * @returns A `URL` object representing the request URL.
79116 */
80- export function createRequestUrl ( nodeRequest : IncomingMessage | Http2ServerRequest ) : URL {
117+ export function createRequestUrl (
118+ nodeRequest : IncomingMessage | Http2ServerRequest ,
119+ trustProxyHeaders ?: boolean | ReadonlySet < string > ,
120+ ) : URL {
81121 const {
82122 headers,
83123 socket,
84124 url = '' ,
85125 originalUrl,
86126 } = nodeRequest as IncomingMessage & { originalUrl ?: string } ;
127+
87128 const protocol =
88- getFirstHeaderValue ( headers [ 'x-forwarded-proto' ] ) ??
129+ getAllowedProxyHeaderValue ( headers , 'x-forwarded-proto' , trustProxyHeaders ) ??
89130 ( 'encrypted' in socket && socket . encrypted ? 'https' : 'http' ) ;
131+
90132 const hostname =
91- getFirstHeaderValue ( headers [ 'x-forwarded-host' ] ) ?? headers . host ?? headers [ ':authority' ] ;
133+ getAllowedProxyHeaderValue ( headers , 'x-forwarded-host' , trustProxyHeaders ) ??
134+ headers . host ??
135+ headers [ ':authority' ] ;
92136
93137 if ( Array . isArray ( hostname ) ) {
94138 throw new Error ( 'host value cannot be an array.' ) ;
95139 }
96140
97141 let hostnameWithPort = hostname ;
98142 if ( ! hostname ?. includes ( ':' ) ) {
99- const port = getFirstHeaderValue ( headers [ 'x-forwarded-port' ] ) ;
143+ const port = getAllowedProxyHeaderValue ( headers , 'x-forwarded-port' , trustProxyHeaders ) ;
100144 if ( port ) {
101145 hostnameWithPort += `:${ port } ` ;
102146 }
103147 }
104148
105149 return new URL ( `${ protocol } ://${ hostnameWithPort } ${ originalUrl ?? url } ` ) ;
106150}
151+
152+ /**
153+ * Gets the first value of an allowed proxy header.
154+ *
155+ * @param headers - The Node.js incoming HTTP headers.
156+ * @param headerName - The name of the proxy header to retrieve.
157+ * @param trustProxyHeaders - A boolean or a set of allowed proxy headers.
158+ * @returns The value of the allowed proxy header, or `undefined` if not allowed or not present.
159+ */
160+ function getAllowedProxyHeaderValue (
161+ headers : IncomingHttpHeaders ,
162+ headerName : string ,
163+ trustProxyHeaders : boolean | ReadonlySet < string > | undefined ,
164+ ) : string | undefined {
165+ return isProxyHeaderAllowed ( headerName , trustProxyHeaders )
166+ ? getFirstHeaderValue ( headers [ headerName ] )
167+ : undefined ;
168+ }
169+
170+ /**
171+ * Checks if a specific proxy header is allowed.
172+ *
173+ * @param headerName - The name of the proxy header to check.
174+ * @param allowedProxyHeaders - A boolean or a set of allowed proxy headers.
175+ * @returns `true` if the header is allowed, `false` otherwise.
176+ */
177+ function isProxyHeaderAllowed (
178+ headerName : string ,
179+ trustProxyHeaders : boolean | ReadonlySet < string > | undefined ,
180+ ) : boolean {
181+ if ( trustProxyHeaders === undefined ) {
182+ const lower = headerName . toLowerCase ( ) ;
183+
184+ return lower === 'x-forwarded-host' || lower === 'x-forwarded-proto' ;
185+ }
186+
187+ if ( trustProxyHeaders === false ) {
188+ return false ;
189+ }
190+
191+ if ( trustProxyHeaders === true ) {
192+ return true ;
193+ }
194+
195+ return trustProxyHeaders . has ( headerName . toLowerCase ( ) ) ;
196+ }
0 commit comments