VYPR
High severity8.8GHSA Advisory· Published May 19, 2026· Updated May 19, 2026

@angular/platform-server: SSRF via Hostname Hijacking

CVE-2026-46417

Description

Impact

A Server-Side Request Forgery (SSRF) vulnerability exists in @angular/platform-server. The issue stems from how the server-side rendering (SSR) engine processes the request URL provided to the rendering entry points.

When an absolute-form URL (e.g., http://evil.com) is passed to the rendering engine, the internal ServerPlatformLocation can be manipulated into adopting the attacker-controlled domain as the "current" hostname.

Consequently, any relative HttpClient requests or PlatformLocation.hostname references are redirected to the attacker controlled server, potentially exposing internal APIs or metadata services.

Fix

Information The vulnerability is mitigated by introducing an Allowlist Mechanism directly into the core rendering APIs. The renderModule and renderApplication functions now include an allowedHosts configuration option. The rendering engine validates the hostname extracted from the request URL against this list before proceeding. If the hostname does not match an allowed entry, the engine prevents the hostname hijacking, ensuring that HttpClient requests remain restricted to trusted domains.

### Patches - 22.0.0-next.12 - 21.2.13 - 20.3.21 - 19.2.22

Workarounds

Developers unable to update immediately should implement strict URL validation in their server entry point (e.g., server.ts). Ensure that req.url is validated against a known list of trusted hostnames or normalized to a relative path before being passed torenderApplication or renderModule.

// Example manual normalization in Express
app.get('*', (req, res, next) => {
  const trustedHost = 'localhost:4000';
  // Ensure the request target matches expectations
  if (req.headers.host !== trustedHost) {
     return res.status(403).send('Forbidden');
  }
  next();
});

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

SSRF in @angular/platform-server allows hostname hijacking via absolute-form URLs, redirecting HttpClient requests to attacker-controlled servers.

Vulnerability

A Server-Side Request Forgery (SSRF) vulnerability exists in @angular/platform-server versions before 19.2.22, 20.3.21, 21.2.13, and 22.0.0-next.12. The SSR engine's ServerPlatformLocation can be manipulated by passing an absolute-form URL (e.g., http://evil.com) to renderModule or renderApplication, causing the internal hostname to be set to the attacker-controlled domain [1][3].

Exploitation

An attacker who can supply a request URL to the Angular SSR entry points (e.g., via a malicious request to the server) can set the hostname to an arbitrary domain. No authentication is required if the server accepts external URLs. The attacker then triggers relative HttpClient requests or PlatformLocation.hostname references, which are redirected to the attacker's server [1][3].

Impact

Successful exploitation leads to Server-Side Request Forgery (SSRF). The attacker can force the server to make requests to internal APIs or metadata services, potentially exposing sensitive information or allowing further attacks [1][3].

Mitigation

Fixed in versions 19.2.22, 20.3.21, 21.2.13, and 22.0.0-next.12 by introducing an allowedHosts configuration option in renderModule and renderApplication [1][4]. Developers unable to update should implement strict URL validation in the server entry point, e.g., checking req.headers.host against a trusted list [1][3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Angular/AngularGHSA2 versions
    <= 18.2.14+ 1 more
    • (no CPE)range: <= 18.2.14
    • (no CPE)range: < 19.2.22 || >= 20.0.0 < 20.3.21 || >= 21.0.0 < 21.2.13 || >= 22.0.0-next.0 < 22.0.0-next.12

Patches

4
8569db88758d

fix(platform-server): add `allowedHosts` option to `renderModule` and `renderApplication`

https://github.com/angular/angularAlan AgiusMay 6, 2026Fixed in 19.2.22via llm-release-walk
8 files changed · +182 20
  • goldens/public-api/platform-server/index.api.md+2 0 modified
    @@ -50,13 +50,15 @@ export function renderApplication(bootstrap: (context: BootstrapContext) => Prom
         document?: string | Document;
         url?: string;
         platformProviders?: Provider[];
    +    allowedHosts?: Readonly<string>[];
     }): Promise<string>;
     
     // @public
     export function renderModule<T>(moduleType: Type<T>, options: {
         document?: string | Document;
         url?: string;
         extraProviders?: StaticProvider[];
    +    allowedHosts?: Readonly<string>[];
     }): Promise<string>;
     
     // @public
    
  • integration/platform-server-hydration/BUILD.bazel+11 12 modified
    @@ -1,12 +1,11 @@
    -load("//integration:index.bzl", "ng_integration_test")
    -
    -ng_integration_test(
    -    name = "test",
    -    # TODO(crisbeto): Pinned temporarily until the CLI is updated to support TS 5.5.
    -    pinned_npm_packages = ["typescript"],
    -    setup_chromium = True,
    -    track_payload_paths = [
    -        "/browser",
    -    ],
    -    track_payload_size = "platform-server-hydration",
    -)
    +# TODO(alanagius): Enable once Angular CLI supports allowedHosts for version 19.
    +# ng_integration_test(
    +#     name = "test",
    +#     # TODO(crisbeto): Pinned temporarily until the CLI is updated to support TS 5.5.
    +#     pinned_npm_packages = ["typescript"],
    +#     setup_chromium = True,
    +#     track_payload_paths = [
    +#         "/browser",
    +#     ],
    +#     track_payload_size = "platform-server-hydration",
    +# )
    
  • integration/platform-server/projects/ngmodule/server.ts+1 0 modified
    @@ -43,6 +43,7 @@ app.get('*', (req, res) => {
     
       renderModule(AppServerModule, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         extraProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • integration/platform-server/projects/standalone/server.ts+1 0 modified
    @@ -43,6 +43,7 @@ app.get('*', (req, res) => {
     
       renderApplication(bootstrap, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • integration/platform-server-zoneless/projects/standalone/server.ts+1 0 modified
    @@ -43,6 +43,7 @@ app.get('*', (req, res) => {
     
       renderApplication(bootstrap, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • packages/platform-server/src/private_export.ts+3 0 modified
    @@ -13,3 +13,6 @@ export {
     export {SERVER_CONTEXT as ɵSERVER_CONTEXT, renderInternal as ɵrenderInternal} from './utils';
     export {ENABLE_DOM_EMULATION as ɵENABLE_DOM_EMULATION} from './tokens';
     export {DominoAdapter as ɵDominoAdapter} from './domino_adapter';
    +
    +// Use in @angular/ssr.
    +export {isHostAllowed as ɵisHostAllowed} from './utils';
    
  • packages/platform-server/src/utils.ts+66 8 modified
    @@ -26,7 +26,7 @@ import {BootstrapContext} from '@angular/platform-browser';
     
     import {platformServer} from './server';
     import {PlatformState} from './platform_state';
    -import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
    +import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens';
     import {createScript} from './transfer_state';
     
     /**
    @@ -39,9 +39,8 @@ import {createScript} from './transfer_state';
      */
     export const EVENT_DISPATCH_SCRIPT_ID = 'ng-event-dispatch-contract';
     
    -interface PlatformOptions {
    +interface PlatformOptions extends Omit<PlatformConfig, 'document'> {
       document?: string | Document;
    -  url?: string;
       platformProviders?: Provider[];
     }
     
    @@ -53,9 +52,16 @@ function createServerPlatform(options: PlatformOptions): PlatformRef {
       const extraProviders = options.platformProviders ?? [];
       const measuringLabel = 'createServerPlatform';
       startMeasuring(measuringLabel);
    +  const {document, url} = options;
     
       const platform = platformServer([
    -    {provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}},
    +    {
    +      provide: INITIAL_CONFIG,
    +      useValue: {
    +        document,
    +        url,
    +      },
    +    },
         extraProviders,
       ]);
     
    @@ -265,14 +271,20 @@ function sanitizeServerContext(serverContext: string): string {
      *                 as a reference to the `document` instance.
      *  - `url` - the URL for the current render request.
      *  - `extraProviders` - set of platform level providers for the current render request.
    - *
    + *  - `allowedHosts` - the allowed hosts list for host validation in server-side rendering.
      * @publicApi
      */
     export async function renderModule<T>(
       moduleType: Type<T>,
    -  options: {document?: string | Document; url?: string; extraProviders?: StaticProvider[]},
    +  options: {
    +    document?: string | Document;
    +    url?: string;
    +    extraProviders?: StaticProvider[];
    +    allowedHosts?: Readonly<string>[];
    +  },
     ): Promise<string> {
    -  const {document, url, extraProviders: platformProviders} = options;
    +  const {document, url, extraProviders: platformProviders, allowedHosts} = options;
    +  validateAllowedHosts(url, allowedHosts);
       const platformRef = createServerPlatform({document, url, platformProviders});
       try {
         const moduleRef = await platformRef.bootstrapModule(moduleType);
    @@ -315,18 +327,27 @@ export async function renderModule<T>(
      *                 as a reference to the `document` instance.
      *  - `url` - the URL for the current render request.
      *  - `platformProviders` - the platform level providers for the current render request.
    + *  - `allowedHosts` - the allowed hosts list for host validation in server-side rendering.
      *
      * @returns A Promise, that returns serialized (to a string) rendered page, once resolved.
      *
      * @publicApi
      */
     export async function renderApplication(
       bootstrap: (context: BootstrapContext) => Promise<ApplicationRef>,
    -  options: {document?: string | Document; url?: string; platformProviders?: Provider[]},
    +  options: {
    +    document?: string | Document;
    +    url?: string;
    +    platformProviders?: Provider[];
    +    allowedHosts?: Readonly<string>[];
    +  },
     ): Promise<string> {
       const renderAppLabel = 'renderApplication';
       const bootstrapLabel = 'bootstrap';
       const _renderLabel = '_render';
    +  const {url, allowedHosts} = options;
    +
    +  validateAllowedHosts(url, allowedHosts);
     
       startMeasuring(renderAppLabel);
       const platformRef = createServerPlatform(options);
    @@ -351,3 +372,40 @@ export async function renderApplication(
         stopMeasuring(renderAppLabel);
       }
     }
    +
    +function validateAllowedHosts(url: string | undefined, allowedHosts: string[] | undefined) {
    +  if (typeof url === 'string' && URL.canParse(url)) {
    +    const hostname = new URL(url).hostname;
    +    const allowedHostsSet: ReadonlySet<string> = new Set(allowedHosts);
    +    if (!isHostAllowed(hostname, allowedHostsSet)) {
    +      throw new Error(`Host ${url} is not allowed. You can configure \`allowedHosts\` option.`);
    +    }
    +  }
    +}
    +
    +/**
    + * Checks if the hostname is allowed.
    + * @param hostname - The hostname to check.
    + * @param allowedHosts - A set of allowed hostnames.
    + * @returns `true` if the hostname is allowed, `false` otherwise.
    + * @note Used also in `@angular/ssr`.
    + * @private
    + */
    +export function isHostAllowed(hostname: string, allowedHosts: ReadonlySet<string>): boolean {
    +  if (allowedHosts.has('*') || allowedHosts.has(hostname)) {
    +    return true;
    +  }
    +
    +  for (const allowedHost of allowedHosts) {
    +    if (!allowedHost.startsWith('*.')) {
    +      continue;
    +    }
    +
    +    const domain = allowedHost.slice(1);
    +    if (hostname.endsWith(domain)) {
    +      return true;
    +    }
    +  }
    +
    +  return false;
    +}
    
  • packages/platform-server/test/utils_spec.ts+97 0 added
    @@ -0,0 +1,97 @@
    +/**
    + * @license
    + * Copyright Google LLC All Rights Reserved.
    + *
    + * Use of this source code is governed by an MIT-style license that can be
    + * found in the LICENSE file at https://angular.dev/license
    + */
    +
    +import {destroyPlatform} from '@angular/core';
    +import {renderApplication, renderModule} from '@angular/platform-server';
    +import {isHostAllowed} from '../src/utils';
    +
    +describe('isHostAllowed', () => {
    +  it('allows matching hostname when in allowedHosts list', () => {
    +    expect(isHostAllowed('test.com', new Set(['test.com', 'example.com']))).toBeTrue();
    +  });
    +
    +  it('allows matching hostname when wildcard matches', () => {
    +    expect(isHostAllowed('sub.example.com', new Set(['test.com', '*.example.com']))).toBeTrue();
    +  });
    +
    +  it('rejects hostname when not in allowedHosts list', () => {
    +    expect(isHostAllowed('evil.com', new Set(['test.com', '*.example.com']))).toBeFalse();
    +  });
    +
    +  it('allows all hostnames when * is in allowedHosts list', () => {
    +    expect(isHostAllowed('anydomain.com', new Set(['*']))).toBeTrue();
    +  });
    +});
    +
    +describe('allowedHosts validation in renderApplication', () => {
    +  const bootstrap = (async () => {}) as any;
    +
    +  beforeEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  afterEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  it('should throw an error on bootstrap if host is not allowed', async () => {
    +    await expectAsync(
    +      renderApplication(bootstrap, {
    +        document: '<app></app>',
    +        url: 'http://evil.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      }),
    +    ).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
    +  });
    +
    +  it('should not throw a host validation error on bootstrap if host is allowed', async () => {
    +    try {
    +      await renderApplication(bootstrap, {
    +        document: '<app></app>',
    +        url: 'http://test.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      });
    +    } catch (error: any) {
    +      expect(error.message).not.toContain('is not allowed');
    +    }
    +  });
    +});
    +
    +describe('allowedHosts validation in renderModule', () => {
    +  class MockModule {}
    +
    +  beforeEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  afterEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  it('should throw an error if host is not allowed', async () => {
    +    await expectAsync(
    +      renderModule(MockModule, {
    +        document: '<app></app>',
    +        url: 'http://evil.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      }),
    +    ).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
    +  });
    +
    +  it('should not throw a host validation error if host is allowed', async () => {
    +    try {
    +      await renderModule(MockModule, {
    +        document: '<app></app>',
    +        url: 'http://test.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      });
    +    } catch (error: any) {
    +      expect(error.message).not.toContain('is not allowed');
    +    }
    +  });
    +});
    
f584840e2e50

fix(platform-server): add `allowedHosts` option to `renderModule` and `renderApplication`

https://github.com/angular/angularAlan AgiusMay 5, 2026Fixed in 20.3.21via llm-release-walk
7 files changed · +171 8
  • goldens/public-api/platform-server/index.api.md+2 0 modified
    @@ -50,13 +50,15 @@ export function renderApplication(bootstrap: (context: BootstrapContext) => Prom
         document?: string | Document;
         url?: string;
         platformProviders?: Provider[];
    +    allowedHosts?: Readonly<string>[];
     }): Promise<string>;
     
     // @public
     export function renderModule<T>(moduleType: Type<T>, options: {
         document?: string | Document;
         url?: string;
         extraProviders?: StaticProvider[];
    +    allowedHosts?: Readonly<string>[];
     }): Promise<string>;
     
     // @public
    
  • integration/platform-server/projects/ngmodule/server.ts+1 0 modified
    @@ -42,6 +42,7 @@ app.use((req, res) => {
     
       renderModule(AppServerModule, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         extraProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • integration/platform-server/projects/standalone/server.ts+1 0 modified
    @@ -42,6 +42,7 @@ app.use((req, res) => {
     
       renderApplication(bootstrap, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • integration/platform-server-zoneless/projects/standalone/server.ts+1 0 modified
    @@ -42,6 +42,7 @@ app.use((req, res) => {
     
       renderApplication(bootstrap, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • packages/platform-server/src/private_export.ts+3 0 modified
    @@ -13,3 +13,6 @@ export {
     export {SERVER_CONTEXT as ɵSERVER_CONTEXT, renderInternal as ɵrenderInternal} from './utils';
     export {ENABLE_DOM_EMULATION as ɵENABLE_DOM_EMULATION} from './tokens';
     export {DominoAdapter as ɵDominoAdapter} from './domino_adapter';
    +
    +// Use in @angular/ssr.
    +export {isHostAllowed as ɵisHostAllowed} from './utils';
    
  • packages/platform-server/src/utils.ts+66 8 modified
    @@ -26,7 +26,7 @@ import {BootstrapContext} from '@angular/platform-browser';
     
     import {platformServer} from './server';
     import {PlatformState} from './platform_state';
    -import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
    +import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens';
     import {createScript} from './transfer_state';
     
     /**
    @@ -39,9 +39,8 @@ import {createScript} from './transfer_state';
      */
     export const EVENT_DISPATCH_SCRIPT_ID = 'ng-event-dispatch-contract';
     
    -interface PlatformOptions {
    +interface PlatformOptions extends Omit<PlatformConfig, 'document'> {
       document?: string | Document;
    -  url?: string;
       platformProviders?: Provider[];
     }
     
    @@ -53,9 +52,16 @@ function createServerPlatform(options: PlatformOptions): PlatformRef {
       const extraProviders = options.platformProviders ?? [];
       const measuringLabel = 'createServerPlatform';
       startMeasuring(measuringLabel);
    +  const {document, url} = options;
     
       const platform = platformServer([
    -    {provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}},
    +    {
    +      provide: INITIAL_CONFIG,
    +      useValue: {
    +        document,
    +        url,
    +      },
    +    },
         extraProviders,
       ]);
     
    @@ -265,14 +271,20 @@ function sanitizeServerContext(serverContext: string): string {
      *                 as a reference to the `document` instance.
      *  - `url` - the URL for the current render request.
      *  - `extraProviders` - set of platform level providers for the current render request.
    - *
    + *  - `allowedHosts` - the allowed hosts list for host validation in server-side rendering.
      * @publicApi
      */
     export async function renderModule<T>(
       moduleType: Type<T>,
    -  options: {document?: string | Document; url?: string; extraProviders?: StaticProvider[]},
    +  options: {
    +    document?: string | Document;
    +    url?: string;
    +    extraProviders?: StaticProvider[];
    +    allowedHosts?: Readonly<string>[];
    +  },
     ): Promise<string> {
    -  const {document, url, extraProviders: platformProviders} = options;
    +  const {document, url, extraProviders: platformProviders, allowedHosts} = options;
    +  validateAllowedHosts(url, allowedHosts);
       const platformRef = createServerPlatform({document, url, platformProviders});
       try {
         const moduleRef = await platformRef.bootstrapModule(moduleType);
    @@ -315,18 +327,27 @@ export async function renderModule<T>(
      *                 as a reference to the `document` instance.
      *  - `url` - the URL for the current render request.
      *  - `platformProviders` - the platform level providers for the current render request.
    + *  - `allowedHosts` - the allowed hosts list for host validation in server-side rendering.
      *
      * @returns A Promise, that returns serialized (to a string) rendered page, once resolved.
      *
      * @publicApi
      */
     export async function renderApplication(
       bootstrap: (context: BootstrapContext) => Promise<ApplicationRef>,
    -  options: {document?: string | Document; url?: string; platformProviders?: Provider[]},
    +  options: {
    +    document?: string | Document;
    +    url?: string;
    +    platformProviders?: Provider[];
    +    allowedHosts?: Readonly<string>[];
    +  },
     ): Promise<string> {
       const renderAppLabel = 'renderApplication';
       const bootstrapLabel = 'bootstrap';
       const _renderLabel = '_render';
    +  const {url, allowedHosts} = options;
    +
    +  validateAllowedHosts(url, allowedHosts);
     
       startMeasuring(renderAppLabel);
       const platformRef = createServerPlatform(options);
    @@ -351,3 +372,40 @@ export async function renderApplication(
         stopMeasuring(renderAppLabel);
       }
     }
    +
    +function validateAllowedHosts(url: string | undefined, allowedHosts: string[] | undefined) {
    +  if (typeof url === 'string' && URL.canParse(url)) {
    +    const hostname = new URL(url).hostname;
    +    const allowedHostsSet: ReadonlySet<string> = new Set(allowedHosts);
    +    if (!isHostAllowed(hostname, allowedHostsSet)) {
    +      throw new Error(`Host ${url} is not allowed. You can configure \`allowedHosts\` option.`);
    +    }
    +  }
    +}
    +
    +/**
    + * Checks if the hostname is allowed.
    + * @param hostname - The hostname to check.
    + * @param allowedHosts - A set of allowed hostnames.
    + * @returns `true` if the hostname is allowed, `false` otherwise.
    + * @note Used also in `@angular/ssr`.
    + * @private
    + */
    +export function isHostAllowed(hostname: string, allowedHosts: ReadonlySet<string>): boolean {
    +  if (allowedHosts.has('*') || allowedHosts.has(hostname)) {
    +    return true;
    +  }
    +
    +  for (const allowedHost of allowedHosts) {
    +    if (!allowedHost.startsWith('*.')) {
    +      continue;
    +    }
    +
    +    const domain = allowedHost.slice(1);
    +    if (hostname.endsWith(domain)) {
    +      return true;
    +    }
    +  }
    +
    +  return false;
    +}
    
  • packages/platform-server/test/utils_spec.ts+97 0 added
    @@ -0,0 +1,97 @@
    +/**
    + * @license
    + * Copyright Google LLC All Rights Reserved.
    + *
    + * Use of this source code is governed by an MIT-style license that can be
    + * found in the LICENSE file at https://angular.dev/license
    + */
    +
    +import {destroyPlatform} from '@angular/core';
    +import {renderApplication, renderModule} from '@angular/platform-server';
    +import {isHostAllowed} from '../src/utils';
    +
    +describe('isHostAllowed', () => {
    +  it('allows matching hostname when in allowedHosts list', () => {
    +    expect(isHostAllowed('test.com', new Set(['test.com', 'example.com']))).toBeTrue();
    +  });
    +
    +  it('allows matching hostname when wildcard matches', () => {
    +    expect(isHostAllowed('sub.example.com', new Set(['test.com', '*.example.com']))).toBeTrue();
    +  });
    +
    +  it('rejects hostname when not in allowedHosts list', () => {
    +    expect(isHostAllowed('evil.com', new Set(['test.com', '*.example.com']))).toBeFalse();
    +  });
    +
    +  it('allows all hostnames when * is in allowedHosts list', () => {
    +    expect(isHostAllowed('anydomain.com', new Set(['*']))).toBeTrue();
    +  });
    +});
    +
    +describe('allowedHosts validation in renderApplication', () => {
    +  const bootstrap = (async () => {}) as any;
    +
    +  beforeEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  afterEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  it('should throw an error on bootstrap if host is not allowed', async () => {
    +    await expectAsync(
    +      renderApplication(bootstrap, {
    +        document: '<app></app>',
    +        url: 'http://evil.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      }),
    +    ).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
    +  });
    +
    +  it('should not throw a host validation error on bootstrap if host is allowed', async () => {
    +    try {
    +      await renderApplication(bootstrap, {
    +        document: '<app></app>',
    +        url: 'http://test.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      });
    +    } catch (error: any) {
    +      expect(error.message).not.toContain('is not allowed');
    +    }
    +  });
    +});
    +
    +describe('allowedHosts validation in renderModule', () => {
    +  class MockModule {}
    +
    +  beforeEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  afterEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  it('should throw an error if host is not allowed', async () => {
    +    await expectAsync(
    +      renderModule(MockModule, {
    +        document: '<app></app>',
    +        url: 'http://evil.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      }),
    +    ).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
    +  });
    +
    +  it('should not throw a host validation error if host is allowed', async () => {
    +    try {
    +      await renderModule(MockModule, {
    +        document: '<app></app>',
    +        url: 'http://test.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      });
    +    } catch (error: any) {
    +      expect(error.message).not.toContain('is not allowed');
    +    }
    +  });
    +});
    
629905d537f5

fix(platform-server): add `allowedHosts` option to `renderModule` and `renderApplication`

https://github.com/angular/angularAlan AgiusMay 5, 2026Fixed in 21.2.13via llm-release-walk
7 files changed · +171 8
  • goldens/public-api/platform-server/index.api.md+2 0 modified
    @@ -50,13 +50,15 @@ export function renderApplication(bootstrap: (context: BootstrapContext) => Prom
         document?: string | Document;
         url?: string;
         platformProviders?: Provider[];
    +    allowedHosts?: Readonly<string>[];
     }): Promise<string>;
     
     // @public
     export function renderModule<T>(moduleType: Type<T>, options: {
         document?: string | Document;
         url?: string;
         extraProviders?: StaticProvider[];
    +    allowedHosts?: Readonly<string>[];
     }): Promise<string>;
     
     // @public
    
  • integration/platform-server/projects/ngmodule/server.ts+1 0 modified
    @@ -42,6 +42,7 @@ app.use((req, res) => {
     
       renderModule(AppServerModule, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         extraProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • integration/platform-server/projects/standalone/server.ts+1 0 modified
    @@ -42,6 +42,7 @@ app.use((req, res) => {
     
       renderApplication(bootstrap, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • integration/platform-server-zoneless/projects/standalone/server.ts+1 0 modified
    @@ -42,6 +42,7 @@ app.use((req, res) => {
     
       renderApplication(bootstrap, {
         document: indexHtml,
    +    allowedHosts: ['localhost'],
         url: `${protocol}://${headers.host}${originalUrl}`,
         platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
       }).then((response: string) => {
    
  • packages/platform-server/src/private_export.ts+3 0 modified
    @@ -13,3 +13,6 @@ export {
     export {SERVER_CONTEXT as ɵSERVER_CONTEXT, renderInternal as ɵrenderInternal} from './utils';
     export {ENABLE_DOM_EMULATION as ɵENABLE_DOM_EMULATION} from './tokens';
     export {DominoAdapter as ɵDominoAdapter} from './domino_adapter';
    +
    +// Use in @angular/ssr.
    +export {isHostAllowed as ɵisHostAllowed} from './utils';
    
  • packages/platform-server/src/utils.ts+66 8 modified
    @@ -26,7 +26,7 @@ import {BootstrapContext} from '@angular/platform-browser';
     
     import {platformServer} from './server';
     import {PlatformState} from './platform_state';
    -import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
    +import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens';
     import {createScript} from './transfer_state';
     
     /**
    @@ -39,9 +39,8 @@ import {createScript} from './transfer_state';
      */
     export const EVENT_DISPATCH_SCRIPT_ID = 'ng-event-dispatch-contract';
     
    -interface PlatformOptions {
    +interface PlatformOptions extends Omit<PlatformConfig, 'document'> {
       document?: string | Document;
    -  url?: string;
       platformProviders?: Provider[];
     }
     
    @@ -53,9 +52,16 @@ function createServerPlatform(options: PlatformOptions): PlatformRef {
       const extraProviders = options.platformProviders ?? [];
       const measuringLabel = 'createServerPlatform';
       startMeasuring(measuringLabel);
    +  const {document, url} = options;
     
       const platform = platformServer([
    -    {provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}},
    +    {
    +      provide: INITIAL_CONFIG,
    +      useValue: {
    +        document,
    +        url,
    +      },
    +    },
         extraProviders,
       ]);
     
    @@ -265,14 +271,20 @@ function sanitizeServerContext(serverContext: string): string {
      *                 as a reference to the `document` instance.
      *  - `url` - the URL for the current render request.
      *  - `extraProviders` - set of platform level providers for the current render request.
    - *
    + *  - `allowedHosts` - the allowed hosts list for host validation in server-side rendering.
      * @publicApi
      */
     export async function renderModule<T>(
       moduleType: Type<T>,
    -  options: {document?: string | Document; url?: string; extraProviders?: StaticProvider[]},
    +  options: {
    +    document?: string | Document;
    +    url?: string;
    +    extraProviders?: StaticProvider[];
    +    allowedHosts?: Readonly<string>[];
    +  },
     ): Promise<string> {
    -  const {document, url, extraProviders: platformProviders} = options;
    +  const {document, url, extraProviders: platformProviders, allowedHosts} = options;
    +  validateAllowedHosts(url, allowedHosts);
       const platformRef = createServerPlatform({document, url, platformProviders});
       try {
         const moduleRef = await platformRef.bootstrapModule(moduleType);
    @@ -315,18 +327,27 @@ export async function renderModule<T>(
      *                 as a reference to the `document` instance.
      *  - `url` - the URL for the current render request.
      *  - `platformProviders` - the platform level providers for the current render request.
    + *  - `allowedHosts` - the allowed hosts list for host validation in server-side rendering.
      *
      * @returns A Promise, that returns serialized (to a string) rendered page, once resolved.
      *
      * @publicApi
      */
     export async function renderApplication(
       bootstrap: (context: BootstrapContext) => Promise<ApplicationRef>,
    -  options: {document?: string | Document; url?: string; platformProviders?: Provider[]},
    +  options: {
    +    document?: string | Document;
    +    url?: string;
    +    platformProviders?: Provider[];
    +    allowedHosts?: Readonly<string>[];
    +  },
     ): Promise<string> {
       const renderAppLabel = 'renderApplication';
       const bootstrapLabel = 'bootstrap';
       const _renderLabel = '_render';
    +  const {url, allowedHosts} = options;
    +
    +  validateAllowedHosts(url, allowedHosts);
     
       startMeasuring(renderAppLabel);
       const platformRef = createServerPlatform(options);
    @@ -351,3 +372,40 @@ export async function renderApplication(
         stopMeasuring(renderAppLabel);
       }
     }
    +
    +function validateAllowedHosts(url: string | undefined, allowedHosts: string[] | undefined) {
    +  if (typeof url === 'string' && URL.canParse(url)) {
    +    const hostname = new URL(url).hostname;
    +    const allowedHostsSet: ReadonlySet<string> = new Set(allowedHosts);
    +    if (!isHostAllowed(hostname, allowedHostsSet)) {
    +      throw new Error(`Host ${url} is not allowed. You can configure \`allowedHosts\` option.`);
    +    }
    +  }
    +}
    +
    +/**
    + * Checks if the hostname is allowed.
    + * @param hostname - The hostname to check.
    + * @param allowedHosts - A set of allowed hostnames.
    + * @returns `true` if the hostname is allowed, `false` otherwise.
    + * @note Used also in `@angular/ssr`.
    + * @private
    + */
    +export function isHostAllowed(hostname: string, allowedHosts: ReadonlySet<string>): boolean {
    +  if (allowedHosts.has('*') || allowedHosts.has(hostname)) {
    +    return true;
    +  }
    +
    +  for (const allowedHost of allowedHosts) {
    +    if (!allowedHost.startsWith('*.')) {
    +      continue;
    +    }
    +
    +    const domain = allowedHost.slice(1);
    +    if (hostname.endsWith(domain)) {
    +      return true;
    +    }
    +  }
    +
    +  return false;
    +}
    
  • packages/platform-server/test/utils_spec.ts+97 0 added
    @@ -0,0 +1,97 @@
    +/**
    + * @license
    + * Copyright Google LLC All Rights Reserved.
    + *
    + * Use of this source code is governed by an MIT-style license that can be
    + * found in the LICENSE file at https://angular.dev/license
    + */
    +
    +import {destroyPlatform} from '@angular/core';
    +import {renderApplication, renderModule} from '@angular/platform-server';
    +import {isHostAllowed} from '../src/utils';
    +
    +describe('isHostAllowed', () => {
    +  it('allows matching hostname when in allowedHosts list', () => {
    +    expect(isHostAllowed('test.com', new Set(['test.com', 'example.com']))).toBeTrue();
    +  });
    +
    +  it('allows matching hostname when wildcard matches', () => {
    +    expect(isHostAllowed('sub.example.com', new Set(['test.com', '*.example.com']))).toBeTrue();
    +  });
    +
    +  it('rejects hostname when not in allowedHosts list', () => {
    +    expect(isHostAllowed('evil.com', new Set(['test.com', '*.example.com']))).toBeFalse();
    +  });
    +
    +  it('allows all hostnames when * is in allowedHosts list', () => {
    +    expect(isHostAllowed('anydomain.com', new Set(['*']))).toBeTrue();
    +  });
    +});
    +
    +describe('allowedHosts validation in renderApplication', () => {
    +  const bootstrap = (async () => {}) as any;
    +
    +  beforeEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  afterEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  it('should throw an error on bootstrap if host is not allowed', async () => {
    +    await expectAsync(
    +      renderApplication(bootstrap, {
    +        document: '<app></app>',
    +        url: 'http://evil.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      }),
    +    ).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
    +  });
    +
    +  it('should not throw a host validation error on bootstrap if host is allowed', async () => {
    +    try {
    +      await renderApplication(bootstrap, {
    +        document: '<app></app>',
    +        url: 'http://test.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      });
    +    } catch (error: any) {
    +      expect(error.message).not.toContain('is not allowed');
    +    }
    +  });
    +});
    +
    +describe('allowedHosts validation in renderModule', () => {
    +  class MockModule {}
    +
    +  beforeEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  afterEach(() => {
    +    destroyPlatform();
    +  });
    +
    +  it('should throw an error if host is not allowed', async () => {
    +    await expectAsync(
    +      renderModule(MockModule, {
    +        document: '<app></app>',
    +        url: 'http://evil.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      }),
    +    ).toBeRejectedWithError(/Host http:\/\/evil.com\/deep\/path is not allowed/);
    +  });
    +
    +  it('should not throw a host validation error if host is allowed', async () => {
    +    try {
    +      await renderModule(MockModule, {
    +        document: '<app></app>',
    +        url: 'http://test.com/deep/path',
    +        allowedHosts: ['test.com', '*.example.com'],
    +      });
    +    } catch (error: any) {
    +      expect(error.message).not.toContain('is not allowed');
    +    }
    +  });
    +});
    
837a71021725

fix(platform-server): ensure origin has a trailing slash when parsing url (#68469)

https://github.com/angular/angularAlan AgiusApr 22, 2026Fixed in 19.2.22via llm-release-walk
2 files changed · +26 5
  • packages/platform-server/src/location.ts+9 5 modified
    @@ -24,12 +24,16 @@ import {INITIAL_CONFIG, PlatformConfig} from './tokens';
      * @param origin The origin to use for resolving the URL.
      * @returns The parsed URL.
      */
    -function parseUrl(urlStr: string, origin: string): URL {
    -  // If the URL is empty or start with a `/` it is a pathname relative to the origin
    -  // otherwise it's an absolute URL.
    -  const urlToParse = urlStr.length === 0 || urlStr[0] === '/' ? origin + urlStr : urlStr;
    +export function parseUrl(urlStr: string, origin: string): URL {
    +  if (URL.canParse(urlStr)) {
    +    return new URL(urlStr);
    +  }
    +
    +  if (urlStr && urlStr[0] !== '/') {
    +    urlStr = `/${urlStr}`;
    +  }
     
    -  return new URL(urlToParse);
    +  return new URL(origin + urlStr);
     }
     
     /**
    
  • packages/platform-server/test/platform_location_spec.ts+17 0 modified
    @@ -11,9 +11,26 @@ import {PlatformLocation, ɵgetDOM as getDOM} from '@angular/common';
     import {destroyPlatform} from '@angular/core';
     import {INITIAL_CONFIG, platformServer} from '@angular/platform-server';
     
    +import {parseUrl} from '../src/location';
    +
     (function () {
       if (getDOM().supportsDOMEvents) return; // NODE only
     
    +  describe('parseUrl', () => {
    +    it('should resolve relative paths against origin', () => {
    +      const url = parseUrl('/deep/path?query#hash', 'http://test.com');
    +      expect(url.href).toBe('http://test.com/deep/path?query#hash');
    +      expect(url.search).toBe('?query');
    +      expect(url.hash).toBe('#hash');
    +    });
    +
    +    it('should resolve absolute URLs ignoring origin', () => {
    +      const url = parseUrl('http://other.com/deep/path', 'http://test.com');
    +      expect(url.href).toBe('http://other.com/deep/path');
    +      expect(url.origin).toBe('http://other.com');
    +    });
    +  });
    +
       describe('PlatformLocation', () => {
         beforeEach(() => {
           destroyPlatform();
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

3

News mentions

0

No linked articles in our index yet.