@@ -16,11 +16,6 @@ const VALID_PORT_REGEX = /^\d+$/;
1616 */
1717const VALID_PROTO_REGEX = / ^ h t t p s ? $ / i;
1818
19- /**
20- * Regular expression to match and remove the `www.` prefix from hostnames.
21- */
22- const WWW_HOST_REGEX = / ^ w w w \. / i;
23-
2419/**
2520 * Regular expression to match path separators.
2621 */
@@ -58,29 +53,36 @@ export function getFirstHeaderValue(
5853}
5954
6055/**
61- * Validates the headers of an incoming request.
62- *
63- * This function checks for the validity of critical headers such as `x-forwarded-host`,
64- * `host`, `x-forwarded-port`, and `x-forwarded-proto`.
65- * It ensures that the hostnames match the allowed hosts and that ports and protocols adhere to expected formats.
56+ * Validates a request.
6657 *
67- * @param request - The incoming `Request` object containing the headers to validate.
58+ * @param request - The incoming `Request` object to validate.
6859 * @param allowedHosts - A set of allowed hostnames.
6960 * @throws Error if any of the validated headers contain invalid values.
7061 */
71- export function validateHeaders ( request : Request , allowedHosts : ReadonlySet < string > ) : void {
72- const headers = request . headers ;
73- validateHost ( 'x-forwarded-host' , headers , allowedHosts ) ;
74- validateHost ( 'host' , headers , allowedHosts ) ;
62+ export function validateRequest ( request : Request , allowedHosts : ReadonlySet < string > ) : void {
63+ validateHeaders ( request , allowedHosts ) ;
64+ validateUrl ( new URL ( request . url ) , allowedHosts ) ;
65+ }
7566
76- const xForwardedPort = getFirstHeaderValue ( headers . get ( 'x-forwarded-port' ) ) ;
77- if ( xForwardedPort && ! VALID_PORT_REGEX . test ( xForwardedPort ) ) {
78- throw new Error ( 'Header "x-forwarded-port" must be a numeric value.' ) ;
79- }
67+ /**
68+ * Validates that the hostname of a given URL is allowed.
69+ *
70+ * @param url - The URL object to validate.
71+ * @param allowedHosts - A set of allowed hostnames.
72+ * @throws Error if the hostname is not in the allowlist.
73+ */
74+ export function validateUrl ( url : URL , allowedHosts : ReadonlySet < string > ) : void {
75+ const { hostname } = url ;
76+ if ( ! isHostAllowed ( hostname , allowedHosts ) ) {
77+ let errorMessage = `URL with hostname "${ hostname } " is not allowed.` ;
78+ if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
79+ errorMessage +=
80+ '\n\nAction Required: Update your "angular.json" to include this hostname. ' +
81+ 'Path: "projects.[project-name].architect.build.options.security.allowedHosts".' +
82+ '\n\nFor more information, see https://angular.dev/guide/ssr#configuring-allowed-hosts' ;
83+ }
8084
81- const xForwardedProto = getFirstHeaderValue ( headers . get ( 'x-forwarded-proto' ) ) ;
82- if ( xForwardedProto && ! VALID_PROTO_REGEX . test ( xForwardedProto ) ) {
83- throw new Error ( 'Header "x-forwarded-proto" must be either "http" or "https".' ) ;
85+ throw new Error ( errorMessage ) ;
8486 }
8587}
8688
@@ -92,12 +94,12 @@ export function validateHeaders(request: Request, allowedHosts: ReadonlySet<stri
9294 * @param allowedHosts - A set of allowed hostnames.
9395 * @throws Error if the header value is invalid or the hostname is not in the allowlist.
9496 */
95- function validateHost (
97+ function validateHostHeaders (
9698 headerName : string ,
9799 headers : Headers ,
98100 allowedHosts : ReadonlySet < string > ,
99101) : void {
100- const value = getFirstHeaderValue ( headers . get ( headerName ) ) ?. replace ( WWW_HOST_REGEX , '' ) ;
102+ const value = getFirstHeaderValue ( headers . get ( headerName ) ) ;
101103 if ( ! value ) {
102104 return ;
103105 }
@@ -113,25 +115,34 @@ function validateHost(
113115 }
114116
115117 const { hostname } = new URL ( url ) ;
116- if (
118+ if ( ! isHostAllowed ( hostname , allowedHosts ) ) {
119+ let errorMessage = `Header "${ headerName } " with value "${ value } " is not allowed.` ;
120+ if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
121+ errorMessage +=
122+ '\n\nAction Required: Update your "angular.json" to include this hostname. ' +
123+ 'Path: "projects.[project-name].architect.build.options.security.allowedHosts".' +
124+ '\n\nFor more information, see https://angular.dev/guide/ssr#configuring-allowed-hosts' ;
125+ }
126+
127+ throw new Error ( errorMessage ) ;
128+ }
129+ }
130+
131+ /**
132+ * Checks if the hostname is allowed.
133+ * @param hostname - The hostname to check.
134+ * @param allowedHosts - A set of allowed hostnames.
135+ * @returns `true` if the hostname is allowed, `false` otherwise.
136+ */
137+ export function isHostAllowed ( hostname : string , allowedHosts : ReadonlySet < string > ) : boolean {
138+ return (
117139 // Check the provided allowed hosts first.
118140 allowedHosts . has ( hostname ) ||
119141 checkWildcardHostnames ( hostname , allowedHosts ) ||
120142 // Check the default allowed hosts last this is the fallback and should be rarely if ever used in production.
121143 DEFAULT_ALLOWED_HOSTS . has ( hostname ) ||
122144 checkWildcardHostnames ( hostname , DEFAULT_ALLOWED_HOSTS )
123- ) {
124- return ;
125- }
126-
127- let errorMessage = `Header "${ headerName } " with value "${ value } " is not allowed.` ;
128- if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
129- errorMessage +=
130- '\n\nAction Required: Update your "angular.json" to include this hostname. ' +
131- 'Path: "projects.[project-name].architect.build.options.security.allowedHosts".' ;
132- }
133-
134- throw new Error ( errorMessage ) ;
145+ ) ;
135146}
136147
137148/**
@@ -154,3 +165,30 @@ function checkWildcardHostnames(hostname: string, allowedHosts: ReadonlySet<stri
154165
155166 return false ;
156167}
168+
169+ /**
170+ * Validates the headers of an incoming request.
171+ *
172+ * This function checks for the validity of critical headers such as `x-forwarded-host`,
173+ * `host`, `x-forwarded-port`, and `x-forwarded-proto`.
174+ * It ensures that the hostnames match the allowed hosts and that ports and protocols adhere to expected formats.
175+ *
176+ * @param request - The incoming `Request` object containing the headers to validate.
177+ * @param allowedHosts - A set of allowed hostnames.
178+ * @throws Error if any of the validated headers contain invalid values.
179+ */
180+ function validateHeaders ( request : Request , allowedHosts : ReadonlySet < string > ) : void {
181+ const headers = request . headers ;
182+ validateHostHeaders ( 'x-forwarded-host' , headers , allowedHosts ) ;
183+ validateHostHeaders ( 'host' , headers , allowedHosts ) ;
184+
185+ const xForwardedPort = getFirstHeaderValue ( headers . get ( 'x-forwarded-port' ) ) ;
186+ if ( xForwardedPort && ! VALID_PORT_REGEX . test ( xForwardedPort ) ) {
187+ throw new Error ( 'Header "x-forwarded-port" must be a numeric value.' ) ;
188+ }
189+
190+ const xForwardedProto = getFirstHeaderValue ( headers . get ( 'x-forwarded-proto' ) ) ;
191+ if ( xForwardedProto && ! VALID_PROTO_REGEX . test ( xForwardedProto ) ) {
192+ throw new Error ( 'Header "x-forwarded-proto" must be either "http" or "https".' ) ;
193+ }
194+ }
0 commit comments