88
99import type { IncomingHttpHeaders , IncomingMessage } from 'node:http' ;
1010import type { Http2ServerRequest } from 'node:http2' ;
11- import { getFirstHeaderValue } from '../../src/utils/validation' ;
11+ import {
12+ getFirstHeaderValue ,
13+ isProxyHeaderAllowed ,
14+ normalizeTrustProxyHeaders ,
15+ } from '../../src/utils/validation' ;
1216
1317/**
1418 * A set containing all the pseudo-headers defined in the HTTP/2 specification.
@@ -17,7 +21,13 @@ import { getFirstHeaderValue } from '../../src/utils/validation';
1721 * as they are not allowed to be set directly using the `Node.js` Undici API or
1822 * the web `Headers` API.
1923 */
20- const HTTP2_PSEUDO_HEADERS = new Set ( [ ':method' , ':scheme' , ':authority' , ':path' , ':status' ] ) ;
24+ const HTTP2_PSEUDO_HEADERS : ReadonlySet < string > = new Set ( [
25+ ':method' ,
26+ ':scheme' ,
27+ ':authority' ,
28+ ':path' ,
29+ ':status' ,
30+ ] ) ;
2131
2232/**
2333 * Converts a Node.js `IncomingMessage` or `Http2ServerRequest` into a
@@ -27,16 +37,25 @@ const HTTP2_PSEUDO_HEADERS = new Set([':method', ':scheme', ':authority', ':path
2737 * be used by web platform APIs.
2838 *
2939 * @param nodeRequest - The Node.js request object (`IncomingMessage` or `Http2ServerRequest`) to convert.
40+ * @param trustProxyHeaders - A boolean or an array of proxy headers to trust when constructing the request URL.
41+ *
42+ * @remarks
43+ * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and
44+ * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure
45+ * level (e.g., at the reverse proxy or API gateway) before reaching the application.
46+ *
3047 * @returns A Web Standard `Request` object.
3148 */
3249export function createWebRequestFromNodeRequest (
3350 nodeRequest : IncomingMessage | Http2ServerRequest ,
51+ trustProxyHeaders ?: boolean | readonly string [ ] ,
3452) : Request {
53+ const trustProxyHeadersNormalized = normalizeTrustProxyHeaders ( trustProxyHeaders ) ;
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,
4160 headers : createRequestHeaders ( headers ) ,
4261 body : withBody ? nodeRequest : undefined ,
@@ -75,32 +94,64 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
7594 * Creates a `URL` object from a Node.js `IncomingMessage`, taking into account the protocol, host, and port.
7695 *
7796 * @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from.
97+ * @param trustProxyHeaders - A set of allowed proxy headers.
98+ *
99+ * @remarks
100+ * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and
101+ * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure
102+ * level (e.g., at the reverse proxy or API gateway) before reaching the application.
103+ *
78104 * @returns A `URL` object representing the request URL.
79105 */
80- export function createRequestUrl ( nodeRequest : IncomingMessage | Http2ServerRequest ) : URL {
106+ export function createRequestUrl (
107+ nodeRequest : IncomingMessage | Http2ServerRequest ,
108+ trustProxyHeaders : ReadonlySet < string > ,
109+ ) : URL {
81110 const {
82111 headers,
83112 socket,
84113 url = '' ,
85114 originalUrl,
86115 } = nodeRequest as IncomingMessage & { originalUrl ?: string } ;
116+
87117 const protocol =
88- getFirstHeaderValue ( headers [ 'x-forwarded-proto' ] ) ??
118+ getAllowedProxyHeaderValue ( headers , 'x-forwarded-proto' , trustProxyHeaders ) ??
89119 ( 'encrypted' in socket && socket . encrypted ? 'https' : 'http' ) ;
120+
90121 const hostname =
91- getFirstHeaderValue ( headers [ 'x-forwarded-host' ] ) ?? headers . host ?? headers [ ':authority' ] ;
122+ getAllowedProxyHeaderValue ( headers , 'x-forwarded-host' , trustProxyHeaders ) ??
123+ headers . host ??
124+ headers [ ':authority' ] ;
92125
93126 if ( Array . isArray ( hostname ) ) {
94127 throw new Error ( 'host value cannot be an array.' ) ;
95128 }
96129
97130 let hostnameWithPort = hostname ;
98131 if ( ! hostname ?. includes ( ':' ) ) {
99- const port = getFirstHeaderValue ( headers [ 'x-forwarded-port' ] ) ;
132+ const port = getAllowedProxyHeaderValue ( headers , 'x-forwarded-port' , trustProxyHeaders ) ;
100133 if ( port ) {
101134 hostnameWithPort += `:${ port } ` ;
102135 }
103136 }
104137
105138 return new URL ( `${ protocol } ://${ hostnameWithPort } ${ originalUrl ?? url } ` ) ;
106139}
140+
141+ /**
142+ * Gets the first value of an allowed proxy header.
143+ *
144+ * @param headers - The Node.js incoming HTTP headers.
145+ * @param headerName - The name of the proxy header to retrieve.
146+ * @param trustProxyHeaders - A set of allowed proxy headers.
147+ * @returns The value of the allowed proxy header, or `undefined` if not allowed or not present.
148+ */
149+ function getAllowedProxyHeaderValue (
150+ headers : IncomingHttpHeaders ,
151+ headerName : string ,
152+ trustProxyHeaders : ReadonlySet < string > ,
153+ ) : string | undefined {
154+ return isProxyHeaderAllowed ( headerName , trustProxyHeaders )
155+ ? getFirstHeaderValue ( headers [ headerName ] )
156+ : undefined ;
157+ }
0 commit comments