@@ -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,35 @@ 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+ if ( ! isHostAllowed ( url . hostname , allowedHosts ) ) {
76+ let errorMessage = `URL with hostname "${ url . hostname } " is not allowed.` ;
77+ if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
78+ errorMessage +=
79+ '\n\nAction Required: Update your "angular.json" to include this hostname. ' +
80+ 'Path: "projects.[project-name].architect.build.options.security.allowedHosts".' +
81+ '\n\nFor more information, see https://angular.dev/guide/ssr#configuring-allowed-hosts' ;
82+ }
8083
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".' ) ;
84+ throw new Error ( errorMessage ) ;
8485 }
8586}
8687
@@ -92,12 +93,12 @@ export function validateHeaders(request: Request, allowedHosts: ReadonlySet<stri
9293 * @param allowedHosts - A set of allowed hostnames.
9394 * @throws Error if the header value is invalid or the hostname is not in the allowlist.
9495 */
95- function validateHost (
96+ function validateHostHeaders (
9697 headerName : string ,
9798 headers : Headers ,
9899 allowedHosts : ReadonlySet < string > ,
99100) : void {
100- const value = getFirstHeaderValue ( headers . get ( headerName ) ) ?. replace ( WWW_HOST_REGEX , '' ) ;
101+ const value = getFirstHeaderValue ( headers . get ( headerName ) ) ;
101102 if ( ! value ) {
102103 return ;
103104 }
@@ -113,25 +114,34 @@ function validateHost(
113114 }
114115
115116 const { hostname } = new URL ( url ) ;
116- if (
117+ if ( ! isHostAllowed ( hostname , allowedHosts ) ) {
118+ let errorMessage = `Header "${ headerName } " with value "${ value } " is not allowed.` ;
119+ if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
120+ errorMessage +=
121+ '\n\nAction Required: Update your "angular.json" to include this hostname. ' +
122+ 'Path: "projects.[project-name].architect.build.options.security.allowedHosts".' +
123+ '\n\nFor more information, see https://angular.dev/guide/ssr#configuring-allowed-hosts' ;
124+ }
125+
126+ throw new Error ( errorMessage ) ;
127+ }
128+ }
129+
130+ /**
131+ * Checks if the hostname is allowed.
132+ * @param hostname - The hostname to check.
133+ * @param allowedHosts - A set of allowed hostnames.
134+ * @returns `true` if the hostname is allowed, `false` otherwise.
135+ */
136+ export function isHostAllowed ( hostname : string , allowedHosts : ReadonlySet < string > ) : boolean {
137+ return (
117138 // Check the provided allowed hosts first.
118139 allowedHosts . has ( hostname ) ||
119140 checkWildcardHostnames ( hostname , allowedHosts ) ||
120141 // Check the default allowed hosts last this is the fallback and should be rarely if ever used in production.
121142 DEFAULT_ALLOWED_HOSTS . has ( hostname ) ||
122143 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 ) ;
144+ ) ;
135145}
136146
137147/**
@@ -154,3 +164,30 @@ function checkWildcardHostnames(hostname: string, allowedHosts: ReadonlySet<stri
154164
155165 return false ;
156166}
167+
168+ /**
169+ * Validates the headers of an incoming request.
170+ *
171+ * This function checks for the validity of critical headers such as `x-forwarded-host`,
172+ * `host`, `x-forwarded-port`, and `x-forwarded-proto`.
173+ * It ensures that the hostnames match the allowed hosts and that ports and protocols adhere to expected formats.
174+ *
175+ * @param request - The incoming `Request` object containing the headers to validate.
176+ * @param allowedHosts - A set of allowed hostnames.
177+ * @throws Error if any of the validated headers contain invalid values.
178+ */
179+ function validateHeaders ( request : Request , allowedHosts : ReadonlySet < string > ) : void {
180+ const headers = request . headers ;
181+ validateHostHeaders ( 'x-forwarded-host' , headers , allowedHosts ) ;
182+ validateHostHeaders ( 'host' , headers , allowedHosts ) ;
183+
184+ const xForwardedPort = getFirstHeaderValue ( headers . get ( 'x-forwarded-port' ) ) ;
185+ if ( xForwardedPort && ! VALID_PORT_REGEX . test ( xForwardedPort ) ) {
186+ throw new Error ( 'Header "x-forwarded-port" must be a numeric value.' ) ;
187+ }
188+
189+ const xForwardedProto = getFirstHeaderValue ( headers . get ( 'x-forwarded-proto' ) ) ;
190+ if ( xForwardedProto && ! VALID_PROTO_REGEX . test ( xForwardedProto ) ) {
191+ throw new Error ( 'Header "x-forwarded-proto" must be either "http" or "https".' ) ;
192+ }
193+ }
0 commit comments