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 * @developerPreview
3249 */
3350export function createWebRequestFromNodeRequest (
3451 nodeRequest : IncomingMessage | Http2ServerRequest ,
52+ trustProxyHeaders ?: boolean | readonly string [ ] ,
3553) : Request {
54+ const trustProxyHeadersNormalized = normalizeTrustProxyHeaders ( trustProxyHeaders ) ;
3655 const { headers, method = 'GET' } = nodeRequest ;
3756 const withBody = method !== 'GET' && method !== 'HEAD' ;
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 ,
@@ -74,32 +93,64 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
7493 * Creates a `URL` object from a Node.js `IncomingMessage`, taking into account the protocol, host, and port.
7594 *
7695 * @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from.
96+ * @param trustProxyHeaders - A set of allowed proxy headers.
97+ *
98+ * @remarks
99+ * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and
100+ * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure
101+ * level (e.g., at the reverse proxy or API gateway) before reaching the application.
102+ *
77103 * @returns A `URL` object representing the request URL.
78104 */
79- export function createRequestUrl ( nodeRequest : IncomingMessage | Http2ServerRequest ) : URL {
105+ export function createRequestUrl (
106+ nodeRequest : IncomingMessage | Http2ServerRequest ,
107+ trustProxyHeaders : ReadonlySet < string > ,
108+ ) : URL {
80109 const {
81110 headers,
82111 socket,
83112 url = '' ,
84113 originalUrl,
85114 } = nodeRequest as IncomingMessage & { originalUrl ?: string } ;
115+
86116 const protocol =
87- getFirstHeaderValue ( headers [ 'x-forwarded-proto' ] ) ??
117+ getAllowedProxyHeaderValue ( headers , 'x-forwarded-proto' , trustProxyHeaders ) ??
88118 ( 'encrypted' in socket && socket . encrypted ? 'https' : 'http' ) ;
119+
89120 const hostname =
90- getFirstHeaderValue ( headers [ 'x-forwarded-host' ] ) ?? headers . host ?? headers [ ':authority' ] ;
121+ getAllowedProxyHeaderValue ( headers , 'x-forwarded-host' , trustProxyHeaders ) ??
122+ headers . host ??
123+ headers [ ':authority' ] ;
91124
92125 if ( Array . isArray ( hostname ) ) {
93126 throw new Error ( 'host value cannot be an array.' ) ;
94127 }
95128
96129 let hostnameWithPort = hostname ;
97130 if ( ! hostname ?. includes ( ':' ) ) {
98- const port = getFirstHeaderValue ( headers [ 'x-forwarded-port' ] ) ;
131+ const port = getAllowedProxyHeaderValue ( headers , 'x-forwarded-port' , trustProxyHeaders ) ;
99132 if ( port ) {
100133 hostnameWithPort += `:${ port } ` ;
101134 }
102135 }
103136
104137 return new URL ( `${ protocol } ://${ hostnameWithPort } ${ originalUrl ?? url } ` ) ;
105138}
139+
140+ /**
141+ * Gets the first value of an allowed proxy header.
142+ *
143+ * @param headers - The Node.js incoming HTTP headers.
144+ * @param headerName - The name of the proxy header to retrieve.
145+ * @param trustProxyHeaders - A set of allowed proxy headers.
146+ * @returns The value of the allowed proxy header, or `undefined` if not allowed or not present.
147+ */
148+ function getAllowedProxyHeaderValue (
149+ headers : IncomingHttpHeaders ,
150+ headerName : string ,
151+ trustProxyHeaders : ReadonlySet < string > ,
152+ ) : string | undefined {
153+ return isProxyHeaderAllowed ( headerName , trustProxyHeaders )
154+ ? getFirstHeaderValue ( headers [ headerName ] )
155+ : undefined ;
156+ }
0 commit comments