From 835c4715f073ce0ebe220b3cad4435c58537a135 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:12:35 +0000 Subject: [PATCH 1/2] fix(@angular/ssr): support '*' in allowedHosts and warn about security risks This commit adds support for '*' in allowedHosts for SSR, allowing any host to be validated. It also adds a security warning when '*' is used to inform users of the potential risks of allowing all host headers. Additionally, it enables '*' for the Vite dev server when 'allowedHosts' is set to 'true'. Closes #32729 --- .../build/src/builders/dev-server/vite/index.ts | 4 +++- packages/angular/ssr/src/app-engine.ts | 16 +++++++++++++++- packages/angular/ssr/src/utils/validation.ts | 2 +- .../angular/ssr/test/utils/validation_spec.ts | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/angular/build/src/builders/dev-server/vite/index.ts b/packages/angular/build/src/builders/dev-server/vite/index.ts index 083008f17050..557b2d34b52a 100644 --- a/packages/angular/build/src/builders/dev-server/vite/index.ts +++ b/packages/angular/build/src/builders/dev-server/vite/index.ts @@ -101,7 +101,9 @@ export async function* serveWithVite( // Angular SSR supports `*.`. const allowedHosts = Array.isArray(serverOptions.allowedHosts) ? serverOptions.allowedHosts.map((host) => (host[0] === '.' ? '*' + host : host)) - : []; + : serverOptions.allowedHosts === true + ? ['*'] + : []; // Always allow the dev server host allowedHosts.push(serverOptions.host); diff --git a/packages/angular/ssr/src/app-engine.ts b/packages/angular/ssr/src/app-engine.ts index f7babe9beaf7..282fa6ac0f7b 100644 --- a/packages/angular/ssr/src/app-engine.ts +++ b/packages/angular/ssr/src/app-engine.ts @@ -88,7 +88,21 @@ export class AngularAppEngine { * @param options Options for the Angular server application engine. */ constructor(options?: AngularAppEngineOptions) { - this.allowedHosts = new Set([...(options?.allowedHosts ?? []), ...this.manifest.allowedHosts]); + this.allowedHosts = this.getAllowedHosts(options); + } + + private getAllowedHosts(options: AngularAppEngineOptions | undefined): ReadonlySet { + const allowedHosts = new Set([...(options?.allowedHosts ?? []), ...this.manifest.allowedHosts]); + + if (allowedHosts.has('*')) { + // eslint-disable-next-line no-console + console.warn( + 'Allowing all hosts via "*" is a security risk. This configuration should only be used when ' + + 'validation for "Host" and "X-Forwarded-Host" headers is performed in another layer.', + ); + } + + return allowedHosts; } /** diff --git a/packages/angular/ssr/src/utils/validation.ts b/packages/angular/ssr/src/utils/validation.ts index cb1bf6ecb56b..e359b94aac6f 100644 --- a/packages/angular/ssr/src/utils/validation.ts +++ b/packages/angular/ssr/src/utils/validation.ts @@ -224,7 +224,7 @@ function verifyHostAllowed( * @returns `true` if the hostname is allowed, `false` otherwise. */ function isHostAllowed(hostname: string, allowedHosts: ReadonlySet): boolean { - if (allowedHosts.has(hostname)) { + if (allowedHosts.has('*') || allowedHosts.has(hostname)) { return true; } diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts index d8c3eaeebdb3..8fed87e83713 100644 --- a/packages/angular/ssr/test/utils/validation_spec.ts +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -64,6 +64,13 @@ describe('Validation Utils', () => { /URL with hostname "google.com" is not allowed/, ); }); + + it('should pass for all hostnames when "*" is used', () => { + const allowedHosts = new Set(['*']); + expect(() => validateUrl(new URL('http://example.com'), allowedHosts)).not.toThrow(); + expect(() => validateUrl(new URL('http://google.com'), allowedHosts)).not.toThrow(); + expect(() => validateUrl(new URL('http://evil.com'), allowedHosts)).not.toThrow(); + }); }); describe('validateRequest', () => { @@ -242,6 +249,15 @@ describe('Validation Utils', () => { expect(secured.headers.get('host')).toBe('example.com'); }); + it('should allow any host header when "*" is used', () => { + const allowedHosts = new Set(['*']); + const req = new Request('http://example.com', { + headers: { 'host': 'evil.com' }, + }); + const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts); + expect(secured.headers.get('host')).toBe('evil.com'); + }); + it('should validate x-forwarded-host header', async () => { const req = new Request('http://example.com', { headers: { 'x-forwarded-host': 'evil.com' }, From a4deca2161a4641547752d0540f49560a3eff36a Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:12:39 +0000 Subject: [PATCH 2/2] fixup! fix(@angular/ssr): support '*' in allowedHosts and warn about security risks --- packages/angular/ssr/src/app-engine.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/angular/ssr/src/app-engine.ts b/packages/angular/ssr/src/app-engine.ts index 282fa6ac0f7b..09e1093fef72 100644 --- a/packages/angular/ssr/src/app-engine.ts +++ b/packages/angular/ssr/src/app-engine.ts @@ -98,7 +98,8 @@ export class AngularAppEngine { // eslint-disable-next-line no-console console.warn( 'Allowing all hosts via "*" is a security risk. This configuration should only be used when ' + - 'validation for "Host" and "X-Forwarded-Host" headers is performed in another layer.', + 'validation for "Host" and "X-Forwarded-Host" headers is performed in another layer, such as a load balancer or reverse proxy. ' + + 'For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf', ); }