From 67582a946808d2c021cbcfacbf203ef58a6fbded Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 23 Feb 2026 10:08:15 +0100 Subject: [PATCH 01/26] fix(@angular/ssr): validate host headers to prevent header-based SSRF This change introduces strict validation for `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-Port` headers in the Angular SSR request handling pipeline, including `CommonEngine` and `AngularAppEngine`. --- goldens/public-api/angular/ssr/index.api.md | 6 + .../public-api/angular/ssr/node/index.api.md | 9 +- .../src/builders/application/execute-build.ts | 3 +- .../build/src/builders/application/options.ts | 3 +- .../src/builders/application/schema.json | 8 + .../src/builders/dev-server/vite-server.ts | 9 + .../src/utils/server-rendering/manifest.ts | 3 + .../src/utils/server-rendering/prerender.ts | 6 + packages/angular/ssr/BUILD.bazel | 2 +- packages/angular/ssr/node/public_api.ts | 2 +- packages/angular/ssr/node/src/app-engine.ts | 43 ++- .../node/src/common-engine/common-engine.ts | 45 +++ .../ssr/node/src/environment-options.ts | 29 ++ packages/angular/ssr/node/src/request.ts | 19 +- packages/angular/ssr/public_api.ts | 2 +- packages/angular/ssr/src/app-engine.ts | 92 ++++++- packages/angular/ssr/src/app.ts | 21 ++ packages/angular/ssr/src/manifest.ts | 6 +- packages/angular/ssr/src/utils/validation.ts | 256 ++++++++++++++++++ packages/angular/ssr/test/app-engine_spec.ts | 158 ++++++++++- .../angular/ssr/test/utils/validation_spec.ts | 240 ++++++++++++++++ .../src/builders/ssr-dev-server/index.ts | 11 +- .../ssr-dev-server/specs/proxy_spec.ts | 3 +- .../builders/ssr-dev-server/specs/ssl_spec.ts | 3 +- .../ssr-dev-server/specs/works_spec.ts | 3 +- .../files/server-builder/server.ts.template | 4 +- packages/schematics/angular/ssr/index.ts | 6 +- .../express-engine-csp-nonce.ts | 1 + .../express-engine-ngmodule.ts | 1 + .../express-engine-standalone.ts | 1 + ...outes-output-mode-server-i18n-base-href.ts | 1 + ...routes-output-mode-server-i18n-sub-path.ts | 1 + .../server-routes-output-mode-server-i18n.ts | 1 + ...tes-output-mode-server-platform-neutral.ts | 3 +- .../server-routes-output-mode-server.ts | 1 + .../server-routes-preload-links.ts | 1 + 36 files changed, 960 insertions(+), 43 deletions(-) create mode 100644 packages/angular/ssr/node/src/environment-options.ts create mode 100644 packages/angular/ssr/src/utils/validation.ts create mode 100644 packages/angular/ssr/test/utils/validation_spec.ts diff --git a/goldens/public-api/angular/ssr/index.api.md b/goldens/public-api/angular/ssr/index.api.md index 81764fcc1f62..cbdacabfd7f6 100644 --- a/goldens/public-api/angular/ssr/index.api.md +++ b/goldens/public-api/angular/ssr/index.api.md @@ -11,11 +11,17 @@ import { Type } from '@angular/core'; // @public export class AngularAppEngine { + constructor(options?: AngularAppEngineOptions); handle(request: Request, requestContext?: unknown): Promise; static ɵallowStaticRouteRender: boolean; static ɵhooks: Hooks; } +// @public +export interface AngularAppEngineOptions { + allowedHosts?: readonly string[]; +} + // @public export function createRequestHandler(handler: RequestHandlerFunction): RequestHandlerFunction; diff --git a/goldens/public-api/angular/ssr/node/index.api.md b/goldens/public-api/angular/ssr/node/index.api.md index eccb6396938e..2c52c06b47c1 100644 --- a/goldens/public-api/angular/ssr/node/index.api.md +++ b/goldens/public-api/angular/ssr/node/index.api.md @@ -15,8 +15,12 @@ import { Type } from '@angular/core'; // @public export class AngularNodeAppEngine { - constructor(); - handle(request: IncomingMessage | Http2ServerRequest, requestContext?: unknown): Promise; + constructor(options?: AngularNodeAppEngineOptions); + handle(request: IncomingMessage | Http2ServerRequest | Request, requestContext?: unknown): Promise; +} + +// @public +export interface AngularNodeAppEngineOptions extends AngularAppEngineOptions { } // @public @@ -27,6 +31,7 @@ export class CommonEngine { // @public (undocumented) export interface CommonEngineOptions { + allowedHosts?: readonly string[]; bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise); enablePerformanceProfiler?: boolean; providers?: StaticProvider[]; diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index 0654cd965558..aaddc5b6ef7e 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -56,6 +56,7 @@ export async function executeBuild( verbose, colors, jsonLogs, + security, } = options; // TODO: Consider integrating into watch mode. Would require full rebuild on target changes. @@ -263,7 +264,7 @@ export async function executeBuild( if (serverEntryPoint) { executionResult.addOutputFile( SERVER_APP_ENGINE_MANIFEST_FILENAME, - generateAngularServerAppEngineManifest(i18nOptions, baseHref), + generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref), BuildOutputFileType.ServerRoot, ); } diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 25bd87253357..87bec31ed381 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -400,8 +400,9 @@ export async function normalizeOptions( } } - const autoCsp = options.security?.autoCsp; + const { autoCsp, allowedHosts = [] } = options.security ?? {}; const security = { + allowedHosts, autoCsp: autoCsp ? { unsafeEval: autoCsp === true ? false : !!autoCsp.unsafeEval, diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 8db4e6145b3f..5498a21fe004 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -52,6 +52,14 @@ "type": "object", "additionalProperties": false, "properties": { + "allowedHosts": { + "description": "A list of hostnames that are allowed to access the server-side application. For more information, see https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "autoCsp": { "description": "Enables automatic generation of a hash-based Strict Content Security Policy (https://web.dev/articles/strict-csp#choose-hash) based on scripts in index.html. Will default to true once we are out of experimental/preview phases.", "default": false, diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 7f8665dd8061..4df3cc3d4023 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -113,7 +113,16 @@ export async function* serveWithVite( } // Disable auto CSP. + const allowedHosts = Array.isArray(serverOptions.allowedHosts) + ? [...serverOptions.allowedHosts] + : []; + + // Always allow the dev server host + allowedHosts.push(serverOptions.host); + browserOptions.security = { + allowedHosts, + // Disable auto CSP. autoCsp: false, }; diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index 2dfad0ff2dfb..975f7718715d 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -53,11 +53,13 @@ function escapeUnsafeChars(str: string): string { * * @param i18nOptions - The internationalization options for the application build. This * includes settings for inlining locales and determining the output structure. + * @param allowedHosts - A list of hosts that are allowed to access the server-side application. * @param baseHref - The base HREF for the application. This is used to set the base URL * for all relative URLs in the application. */ export function generateAngularServerAppEngineManifest( i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'], + allowedHosts: string[], baseHref: string | undefined, ): string { const entryPoints: Record = {}; @@ -84,6 +86,7 @@ export function generateAngularServerAppEngineManifest( const manifestContent = ` export default { basePath: '${basePath}', + allowedHosts: ${JSON.stringify(allowedHosts, undefined, 2)}, supportedLocales: ${JSON.stringify(supportedLocales, undefined, 2)}, entryPoints: { ${Object.entries(entryPoints) diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index f33f851f10c4..39d0f0934c92 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -225,6 +225,9 @@ async function renderPages( hasSsrEntry: !!outputFilesForWorker['server.mjs'], } as RenderWorkerData, execArgv: workerExecArgv, + env: { + 'NG_ALLOWED_HOSTS': 'localhost', + }, }); try { @@ -337,6 +340,9 @@ async function getAllRoutes( hasSsrEntry: !!outputFilesForWorker['server.mjs'], } as RoutesExtractorWorkerData, execArgv: workerExecArgv, + env: { + 'NG_ALLOWED_HOSTS': 'localhost', + }, }); try { diff --git a/packages/angular/ssr/BUILD.bazel b/packages/angular/ssr/BUILD.bazel index 2218488daeb6..982bdfbb577b 100644 --- a/packages/angular/ssr/BUILD.bazel +++ b/packages/angular/ssr/BUILD.bazel @@ -20,7 +20,7 @@ ts_project( ), args = [ "--lib", - "dom,es2020", + "dom.iterable,dom,es2022", ], data = [ "//packages/angular/ssr/third_party/beasties:beasties_bundled", diff --git a/packages/angular/ssr/node/public_api.ts b/packages/angular/ssr/node/public_api.ts index 81932873a440..89900f95cc5c 100644 --- a/packages/angular/ssr/node/public_api.ts +++ b/packages/angular/ssr/node/public_api.ts @@ -12,7 +12,7 @@ export { type CommonEngineOptions, } from './src/common-engine/common-engine'; -export { AngularNodeAppEngine } from './src/app-engine'; +export { AngularNodeAppEngine, type AngularNodeAppEngineOptions } from './src/app-engine'; export { createNodeRequestHandler, type NodeRequestHandlerFunction } from './src/handler'; export { writeResponseToNodeResponse } from './src/response'; diff --git a/packages/angular/ssr/node/src/app-engine.ts b/packages/angular/ssr/node/src/app-engine.ts index 8edac0ef69c6..29eb1fd366ab 100644 --- a/packages/angular/ssr/node/src/app-engine.ts +++ b/packages/angular/ssr/node/src/app-engine.ts @@ -9,9 +9,16 @@ import { AngularAppEngine } from '@angular/ssr'; import type { IncomingMessage } from 'node:http'; import type { Http2ServerRequest } from 'node:http2'; +import { AngularAppEngineOptions } from '../../src/app-engine'; +import { getAllowedHostsFromEnv } from './environment-options'; import { attachNodeGlobalErrorHandlers } from './errors'; import { createWebRequestFromNodeRequest } from './request'; +/** + * Options for the Angular Node.js server application engine. + */ +export interface AngularNodeAppEngineOptions extends AngularAppEngineOptions {} + /** * Angular server application engine. * Manages Angular server applications (including localized ones), handles rendering requests, @@ -21,9 +28,18 @@ import { createWebRequestFromNodeRequest } from './request'; * application to ensure consistent handling of rendering requests and resource management. */ export class AngularNodeAppEngine { - private readonly angularAppEngine = new AngularAppEngine(); + private readonly angularAppEngine: AngularAppEngine; + + /** + * Creates a new instance of the Angular Node.js server application engine. + * @param options Options for the Angular Node.js server application engine. + */ + constructor(options?: AngularNodeAppEngineOptions) { + this.angularAppEngine = new AngularAppEngine({ + ...options, + allowedHosts: [...getAllowedHostsFromEnv(), ...(options?.allowedHosts ?? [])], + }); - constructor() { attachNodeGlobalErrorHandlers(); } @@ -31,21 +47,36 @@ export class AngularNodeAppEngine { * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering, * or delivering a static file for client-side rendered routes based on the `RenderMode` setting. * - * This method adapts Node.js's `IncomingMessage` or `Http2ServerRequest` + * This method adapts Node.js's `IncomingMessage`, `Http2ServerRequest` or `Request` * to a format compatible with the `AngularAppEngine` and delegates the handling logic to it. * - * @param request - The incoming HTTP request (`IncomingMessage` or `Http2ServerRequest`). + * @param request - The incoming HTTP request (`IncomingMessage`, `Http2ServerRequest` or `Request`). * @param requestContext - Optional context for rendering, such as metadata associated with the request. * @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found. * * @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route * corresponding to `https://www.example.com/page`. + * + * @remarks + * To prevent potential Server-Side Request Forgery (SSRF), this function verifies the hostname + * of the `request.url` against a list of authorized hosts. + * If the hostname is not recognized and `allowedHosts` is not empty, a Client-Side Rendered (CSR) version of the + * page is returned otherwise a 400 Bad Request is returned. + * + * Resolution: + * Authorize your hostname by configuring `allowedHosts` in `angular.json` in: + * `projects.[project-name].architect.build.options.security.allowedHosts`. + * Alternatively, you can define the allowed hostname via the environment variable `process.env['NG_ALLOWED_HOSTS']` + * or pass it directly through the configuration options of `AngularNodeAppEngine`. + * + * For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf */ async handle( - request: IncomingMessage | Http2ServerRequest, + request: IncomingMessage | Http2ServerRequest | Request, requestContext?: unknown, ): Promise { - const webRequest = createWebRequestFromNodeRequest(request); + const webRequest = + request instanceof Request ? request : createWebRequestFromNodeRequest(request); return this.angularAppEngine.handle(webRequest, requestContext); } diff --git a/packages/angular/ssr/node/src/common-engine/common-engine.ts b/packages/angular/ssr/node/src/common-engine/common-engine.ts index 079c1187696b..1c130d9abe86 100644 --- a/packages/angular/ssr/node/src/common-engine/common-engine.ts +++ b/packages/angular/ssr/node/src/common-engine/common-engine.ts @@ -12,6 +12,8 @@ import { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/plat import * as fs from 'node:fs'; import { dirname, join, normalize, resolve } from 'node:path'; import { URL } from 'node:url'; +import { validateUrl } from '../../../src/utils/validation'; +import { getAllowedHostsFromEnv } from '../environment-options'; import { attachNodeGlobalErrorHandlers } from '../errors'; import { CommonEngineInlineCriticalCssProcessor } from './inline-css-processor'; import { @@ -31,6 +33,9 @@ export interface CommonEngineOptions { /** Enable request performance profiling data collection and printing the results in the server console. */ enablePerformanceProfiler?: boolean; + + /** A set of hostnames that are allowed to access the server. */ + allowedHosts?: readonly string[]; } export interface CommonEngineRenderOptions { @@ -64,8 +69,14 @@ export class CommonEngine { private readonly templateCache = new Map(); private readonly inlineCriticalCssProcessor = new CommonEngineInlineCriticalCssProcessor(); private readonly pageIsSSG = new Map(); + private readonly allowedHosts: ReadonlySet; constructor(private options?: CommonEngineOptions) { + this.allowedHosts = new Set([ + ...getAllowedHostsFromEnv(), + ...(this.options?.allowedHosts ?? []), + ]); + attachNodeGlobalErrorHandlers(); } @@ -74,6 +85,40 @@ export class CommonEngine { * render options */ async render(opts: CommonEngineRenderOptions): Promise { + const { url } = opts; + + if (url && URL.canParse(url)) { + const urlObj = new URL(url); + try { + validateUrl(urlObj, this.allowedHosts); + } catch (error) { + const isAllowedHostConfigured = this.allowedHosts.size > 0; + // eslint-disable-next-line no-console + console.error( + `ERROR: ${(error as Error).message}` + + 'Please provide a list of allowed hosts in the "allowedHosts" option in the "CommonEngine" constructor.', + isAllowedHostConfigured + ? '' + : '\nFalling back to client side rendering. This will become a 400 Bad Request in a future major version.', + ); + + if (!isAllowedHostConfigured) { + // Fallback to CSR to avoid a breaking change. + // TODO(alanagius): Return a 400 and remove this fallback in the next major version (v22). + let document = opts.document; + if (!document && opts.documentFilePath) { + document = opts.document ?? (await this.getDocument(opts.documentFilePath)); + } + + if (document) { + return document; + } + } + + throw error; + } + } + const enablePerformanceProfiler = this.options?.enablePerformanceProfiler; const runMethod = enablePerformanceProfiler diff --git a/packages/angular/ssr/node/src/environment-options.ts b/packages/angular/ssr/node/src/environment-options.ts new file mode 100644 index 000000000000..b18dcd08ebe6 --- /dev/null +++ b/packages/angular/ssr/node/src/environment-options.ts @@ -0,0 +1,29 @@ +/** + * @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 + */ + +/** + * Retrieves the list of allowed hosts from the environment variable `NG_ALLOWED_HOSTS`. + * @returns An array of allowed hosts. + */ +export function getAllowedHostsFromEnv(): ReadonlyArray { + const allowedHosts: string[] = []; + const envNgAllowedHosts = process.env['NG_ALLOWED_HOSTS']; + if (!envNgAllowedHosts) { + return allowedHosts; + } + + const hosts = envNgAllowedHosts.split(','); + for (const host of hosts) { + const trimmed = host.trim(); + if (trimmed.length > 0) { + allowedHosts.push(trimmed); + } + } + + return allowedHosts; +} diff --git a/packages/angular/ssr/node/src/request.ts b/packages/angular/ssr/node/src/request.ts index 32d90d0029fc..402ec29ba56d 100644 --- a/packages/angular/ssr/node/src/request.ts +++ b/packages/angular/ssr/node/src/request.ts @@ -8,6 +8,7 @@ import type { IncomingHttpHeaders, IncomingMessage } from 'node:http'; import type { Http2ServerRequest } from 'node:http2'; +import { getFirstHeaderValue } from '../../src/utils/validation'; /** * A set containing all the pseudo-headers defined in the HTTP/2 specification. @@ -103,21 +104,3 @@ export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerReque return new URL(`${protocol}://${hostnameWithPort}${originalUrl ?? url}`); } - -/** - * Extracts the first value from a multi-value header string. - * - * @param value - A string or an array of strings representing the header values. - * If it's a string, values are expected to be comma-separated. - * @returns The first trimmed value from the multi-value header, or `undefined` if the input is invalid or empty. - * - * @example - * ```typescript - * getFirstHeaderValue("value1, value2, value3"); // "value1" - * getFirstHeaderValue(["value1", "value2"]); // "value1" - * getFirstHeaderValue(undefined); // undefined - * ``` - */ -function getFirstHeaderValue(value: string | string[] | undefined): string | undefined { - return value?.toString().split(',', 1)[0]?.trim(); -} diff --git a/packages/angular/ssr/public_api.ts b/packages/angular/ssr/public_api.ts index e685f4ceabe3..e566d8414f2f 100644 --- a/packages/angular/ssr/public_api.ts +++ b/packages/angular/ssr/public_api.ts @@ -8,7 +8,7 @@ export * from './private_export'; -export { AngularAppEngine } from './src/app-engine'; +export { AngularAppEngine, type AngularAppEngineOptions } from './src/app-engine'; export { createRequestHandler, type RequestHandlerFunction } from './src/handler'; export { diff --git a/packages/angular/ssr/src/app-engine.ts b/packages/angular/ssr/src/app-engine.ts index dd204a4b595f..6242750b4c47 100644 --- a/packages/angular/ssr/src/app-engine.ts +++ b/packages/angular/ssr/src/app-engine.ts @@ -11,6 +11,17 @@ import { Hooks } from './hooks'; import { getPotentialLocaleIdFromUrl, getPreferredLocale } from './i18n'; import { EntryPointExports, getAngularAppEngineManifest } from './manifest'; import { joinUrlParts } from './utils/url'; +import { cloneRequestAndPatchHeaders, validateRequest } from './utils/validation'; + +/** + * Options for the Angular server application engine. + */ +export interface AngularAppEngineOptions { + /** + * A set of allowed hostnames for the server application. + */ + allowedHosts?: readonly string[]; +} /** * Angular server application engine. @@ -45,6 +56,11 @@ export class AngularAppEngine { */ private readonly manifest = getAngularAppEngineManifest(); + /** + * A set of allowed hostnames for the server application. + */ + private readonly allowedHosts: ReadonlySet; + /** * A map of supported locales from the server application's manifest. */ @@ -57,6 +73,14 @@ export class AngularAppEngine { */ private readonly entryPointsCache = new Map>(); + /** + * Creates a new instance of the Angular server application engine. + * @param options Options for the Angular server application engine. + */ + constructor(options?: AngularAppEngineOptions) { + this.allowedHosts = new Set([...(options?.allowedHosts ?? []), ...this.manifest.allowedHosts]); + } + /** * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering, * or delivering a static file for client-side rendered routes based on the `RenderMode` setting. @@ -67,12 +91,38 @@ export class AngularAppEngine { * * @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route * corresponding to `https://www.example.com/page`. + * + * @remarks + * To prevent potential Server-Side Request Forgery (SSRF), this function verifies the hostname + * of the `request.url` against a list of authorized hosts. + * If the hostname is not recognized and `allowedHosts` is not empty, a Client-Side Rendered (CSR) version of the + * page is returned otherwise a 400 Bad Request is returned. + * Resolution: + * Authorize your hostname by configuring `allowedHosts` in `angular.json` in: + * `projects.[project-name].architect.build.options.security.allowedHosts`. + * Alternatively, you pass it directly through the configuration options of `AngularAppEngine`. + * + * For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf */ async handle(request: Request, requestContext?: unknown): Promise { - const serverApp = await this.getAngularServerAppForRequest(request); + const allowedHost = this.allowedHosts; + try { + validateRequest(request, allowedHost); + } catch (error) { + return this.handleValidationError(error as Error, request); + } + + // Clone request with patched headers to prevent unallowed host header access. + const { request: securedRequest, onError: onHeaderValidationError } = + cloneRequestAndPatchHeaders(request, allowedHost); + + const serverApp = await this.getAngularServerAppForRequest(securedRequest); if (serverApp) { - return serverApp.handle(request, requestContext); + return Promise.race([ + onHeaderValidationError.then((error) => this.handleValidationError(error, securedRequest)), + serverApp.handle(securedRequest, requestContext), + ]); } if (this.supportedLocales.length > 1) { @@ -201,4 +251,42 @@ export class AngularAppEngine { return this.getEntryPointExports(potentialLocale) ?? this.getEntryPointExports(''); } + + /** + * Handles validation errors by logging the error and returning an appropriate response. + * + * @param error - The validation error to handle. + * @param request - The HTTP request that caused the validation error. + * @returns A promise that resolves to a `Response` object with a 400 status code if allowed hosts are configured, + * or `null` if allowed hosts are not configured (in which case the request is served client-side). + */ + private async handleValidationError(error: Error, request: Request): Promise { + const isAllowedHostConfigured = this.allowedHosts.size > 0; + const errorMessage = error.message; + + // eslint-disable-next-line no-console + console.error( + `ERROR: Bad Request ("${request.url}").\n` + + errorMessage + + (isAllowedHostConfigured + ? '' + : '\nFalling back to client side rendering. This will become a 400 Bad Request in a future major version.') + + '\n\nFor more information, see https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf', + ); + + if (isAllowedHostConfigured) { + // Allowed hosts has been configured incorrectly, thus we can return a 400 bad request. + return new Response(errorMessage, { + status: 400, + statusText: 'Bad Request', + headers: { 'Content-Type': 'text/plain' }, + }); + } + + // Fallback to CSR to avoid a breaking change. + // TODO(alanagius): Return a 400 and remove this fallback in the next major version (v22). + const serverApp = await this.getAngularServerAppForRequest(request); + + return serverApp?.serveClientSidePage() ?? null; + } } diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index ee14f8a26105..85227a9d8a33 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -468,6 +468,27 @@ export class AngularServerApp { return html; } + + /** + * Serves the client-side version of the application. + * TODO(alanagius): Remove this method in version 22. + * @internal + */ + async serveClientSidePage(): Promise { + const { + manifest: { locale }, + assets, + } = this; + + const html = await assets.getServerAsset('index.csr.html').text(); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html;charset=UTF-8', + ...(locale !== undefined ? { 'Content-Language': locale } : {}), + }), + }); + } } let angularServerApp: AngularServerApp | undefined; diff --git a/packages/angular/ssr/src/manifest.ts b/packages/angular/ssr/src/manifest.ts index 0de603bba104..21ded49b3e10 100644 --- a/packages/angular/ssr/src/manifest.ts +++ b/packages/angular/ssr/src/manifest.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { BootstrapContext } from '@angular/platform-browser'; import type { SerializableRouteTreeNode } from './routes/route-tree'; import { AngularBootstrap } from './utils/ng'; @@ -74,6 +73,11 @@ export interface AngularAppEngineManifest { * - `value`: The url segment associated with that locale. */ readonly supportedLocales: Readonly>; + + /** + * A readonly array of allowed hostnames. + */ + readonly allowedHosts: Readonly; } /** diff --git a/packages/angular/ssr/src/utils/validation.ts b/packages/angular/ssr/src/utils/validation.ts new file mode 100644 index 000000000000..97ac8606f3b4 --- /dev/null +++ b/packages/angular/ssr/src/utils/validation.ts @@ -0,0 +1,256 @@ +/** + * @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 + */ + +/** + * The set of headers that should be validated for host header injection attacks. + */ +const HOST_HEADERS_TO_VALIDATE: ReadonlySet = new Set(['host', 'x-forwarded-host']); + +/** + * Regular expression to validate that the port is a numeric value. + */ +const VALID_PORT_REGEX = /^\d+$/; + +/** + * Regular expression to validate that the protocol is either http or https (case-insensitive). + */ +const VALID_PROTO_REGEX = /^https?$/i; + +/** + * Regular expression to validate that the host is a valid hostname. + */ +const VALID_HOST_REGEX = /^[a-z0-9.:-]+$/i; + +/** + * Extracts the first value from a multi-value header string. + * + * @param value - A string or an array of strings representing the header values. + * If it's a string, values are expected to be comma-separated. + * @returns The first trimmed value from the multi-value header, or `undefined` if the input is invalid or empty. + * + * @example + * ```typescript + * getFirstHeaderValue("value1, value2, value3"); // "value1" + * getFirstHeaderValue(["value1", "value2"]); // "value1" + * getFirstHeaderValue(undefined); // undefined + * ``` + */ +export function getFirstHeaderValue( + value: string | string[] | undefined | null, +): string | undefined { + return value?.toString().split(',', 1)[0]?.trim(); +} + +/** + * Validates a request. + * + * @param request - The incoming `Request` object to validate. + * @param allowedHosts - A set of allowed hostnames. + * @throws Error if any of the validated headers contain invalid values. + */ +export function validateRequest(request: Request, allowedHosts: ReadonlySet): void { + validateHeaders(request); + validateUrl(new URL(request.url), allowedHosts); +} + +/** + * Validates that the hostname of a given URL is allowed. + * + * @param url - The URL object to validate. + * @param allowedHosts - A set of allowed hostnames. + * @throws Error if the hostname is not in the allowlist. + */ +export function validateUrl(url: URL, allowedHosts: ReadonlySet): void { + const { hostname } = url; + if (!isHostAllowed(hostname, allowedHosts)) { + throw new Error(`URL with hostname "${hostname}" is not allowed.`); + } +} + +/** + * Clones a request and patches the `get` method of the request headers to validate the host headers. + * @param request - The request to validate. + * @param allowedHosts - A set of allowed hostnames. + * @returns An object containing the cloned request and a promise that resolves to an error + * if any of the validated headers contain invalid values. + */ +export function cloneRequestAndPatchHeaders( + request: Request, + allowedHosts: ReadonlySet, +): { request: Request; onError: Promise } { + let onError: (value: Error) => void; + const onErrorPromise = new Promise((resolve) => { + onError = resolve; + }); + + const clonedReq = new Request(request.clone(), { + signal: request.signal, + }); + + const headers = clonedReq.headers; + + const originalGet = headers.get; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (headers.get as typeof originalGet) = function (name) { + const value = originalGet.call(headers, name); + if (!value) { + return value; + } + + validateHeader(name, value, allowedHosts, onError); + + return value; + }; + + const originalValues = headers.values; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (headers.values as typeof originalValues) = function () { + for (const name of HOST_HEADERS_TO_VALIDATE) { + validateHeader(name, originalGet.call(headers, name), allowedHosts, onError); + } + + return originalValues.call(headers); + }; + + const originalEntries = headers.entries; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (headers.entries as typeof originalEntries) = function () { + const iterator = originalEntries.call(headers); + + return { + next() { + const result = iterator.next(); + if (!result.done) { + const [key, value] = result.value; + validateHeader(key, value, allowedHosts, onError); + } + + return result; + }, + [Symbol.iterator]() { + return this; + }, + }; + }; + + // Ensure for...of loops use the new patched entries + (headers[Symbol.iterator] as typeof originalEntries) = headers.entries; + + return { request: clonedReq, onError: onErrorPromise }; +} + +/** + * Validates a specific header value against the allowed hosts. + * @param name - The name of the header to validate. + * @param value - The value of the header to validate. + * @param allowedHosts - A set of allowed hostnames. + * @param onError - A callback function to call if the header value is invalid. + * @throws Error if the header value is invalid. + */ +function validateHeader( + name: string, + value: string | null, + allowedHosts: ReadonlySet, + onError: (value: Error) => void, +): void { + if (!value) { + return; + } + + if (!HOST_HEADERS_TO_VALIDATE.has(name.toLowerCase())) { + return; + } + + try { + verifyHostAllowed(name, value, allowedHosts); + } catch (error) { + onError(error as Error); + + throw error; + } +} + +/** + * Validates a specific host header value against the allowed hosts. + * + * @param headerName - The name of the header to validate (e.g., 'host', 'x-forwarded-host'). + * @param headerValue - The value of the header to validate. + * @param allowedHosts - A set of allowed hostnames. + * @throws Error if the header value is invalid or the hostname is not in the allowlist. + */ +function verifyHostAllowed( + headerName: string, + headerValue: string, + allowedHosts: ReadonlySet, +): void { + const value = getFirstHeaderValue(headerValue); + if (!value) { + return; + } + + const url = `http://${value}`; + if (!URL.canParse(url)) { + throw new Error(`Header "${headerName}" contains an invalid value and cannot be parsed.`); + } + + const { hostname } = new URL(url); + if (!isHostAllowed(hostname, allowedHosts)) { + throw new Error(`Header "${headerName}" with value "${value}" is not allowed.`); + } +} + +/** + * 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. + */ +function isHostAllowed(hostname: string, allowedHosts: ReadonlySet): boolean { + if (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; +} + +/** + * Validates the headers of an incoming request. + * + * @param request - The incoming `Request` object containing the headers to validate. + * @throws Error if any of the validated headers contain invalid values. + */ +function validateHeaders(request: Request): void { + const headers = request.headers; + for (const headerName of HOST_HEADERS_TO_VALIDATE) { + const headerValue = getFirstHeaderValue(headers.get(headerName)); + if (headerValue && !VALID_HOST_REGEX.test(headerValue)) { + throw new Error(`Header "${headerName}" contains characters that are not allowed.`); + } + } + + const xForwardedPort = getFirstHeaderValue(headers.get('x-forwarded-port')); + if (xForwardedPort && !VALID_PORT_REGEX.test(xForwardedPort)) { + throw new Error('Header "x-forwarded-port" must be a numeric value.'); + } + + const xForwardedProto = getFirstHeaderValue(headers.get('x-forwarded-proto')); + if (xForwardedProto && !VALID_PROTO_REGEX.test(xForwardedProto)) { + throw new Error('Header "x-forwarded-proto" must be either "http" or "https".'); + } +} diff --git a/packages/angular/ssr/test/app-engine_spec.ts b/packages/angular/ssr/test/app-engine_spec.ts index 76b68f6d4a82..738f7c695cc3 100644 --- a/packages/angular/ssr/test/app-engine_spec.ts +++ b/packages/angular/ssr/test/app-engine_spec.ts @@ -11,13 +11,26 @@ import '@angular/compiler'; /* eslint-enable import/no-unassigned-import */ -import { Component } from '@angular/core'; +import { Component, REQUEST, inject } from '@angular/core'; import { destroyAngularServerApp, getOrCreateAngularServerApp } from '../src/app'; import { AngularAppEngine } from '../src/app-engine'; import { setAngularAppEngineManifest } from '../src/manifest'; import { RenderMode } from '../src/routes/route-config'; import { setAngularAppTestingManifest } from './testing-utils'; +@Component({ + selector: 'app-home', + template: 'Home works', +}) +class TestHomeComponent { + private request = inject(REQUEST); + constructor() { + // Force header access to trigger validation + this.request?.headers.get('host'); + this.request?.headers.get('x-forwarded-host'); + } +} + function createEntryPoint(locale: string) { return async () => { @Component({ @@ -84,6 +97,7 @@ describe('AngularAppEngine', () => { describe('Localized app', () => { beforeAll(() => { setAngularAppEngineManifest({ + allowedHosts: ['example.com'], // Note: Although we are testing only one locale, we need to configure two or more // to ensure that we test a different code path. entryPoints: { @@ -163,6 +177,7 @@ describe('AngularAppEngine', () => { describe('Localized app with single locale', () => { beforeAll(() => { setAngularAppEngineManifest({ + allowedHosts: ['example.com'], entryPoints: { it: createEntryPoint('it'), }, @@ -230,6 +245,7 @@ describe('AngularAppEngine', () => { class HomeComponent {} setAngularAppEngineManifest({ + allowedHosts: ['example.com'], entryPoints: { '': async () => { setAngularAppTestingManifest( @@ -274,4 +290,144 @@ describe('AngularAppEngine', () => { expect(await response?.text()).toContain('Home works'); }); }); + + describe('Invalid host headers', () => { + let consoleErrorSpy: jasmine.Spy; + + describe('with allowed hosts configured', () => { + beforeAll(() => { + setAngularAppEngineManifest({ + allowedHosts: ['example.com'], + entryPoints: { + '': async () => { + setAngularAppTestingManifest( + [{ path: 'home', component: TestHomeComponent }], + [{ path: '**', renderMode: RenderMode.Server }], + ); + + return { + ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, + ɵdestroyAngularServerApp: destroyAngularServerApp, + }; + }, + }, + basePath: '/', + supportedLocales: { 'en-US': '' }, + }); + + appEngine = new AngularAppEngine(); + }); + + beforeEach(() => { + consoleErrorSpy = spyOn(console, 'error'); + }); + + it('should return 400 when disallowed host', async () => { + const request = new Request('https://evil.com'); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain('URL with hostname "evil.com" is not allowed.'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('URL with hostname "evil.com" is not allowed.'), + ); + }); + + it('should return 400 when disallowed host header', async () => { + const request = new Request('https://example.com/home', { + headers: { 'host': 'evil.com' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain( + 'Header "host" with value "evil.com" is not allowed.', + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), + ); + }); + + it('should return 400 when disallowed x-forwarded-host header', async () => { + const request = new Request('https://example.com/home', { + headers: { 'x-forwarded-host': 'evil.com' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain( + 'Header "x-forwarded-host" with value "evil.com" is not allowed.', + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('Header "x-forwarded-host" with value "evil.com" is not allowed.'), + ); + }); + + it('should return 400 when host with path separator', async () => { + const request = new Request('https://example.com/home', { + headers: { 'host': 'example.com/evil' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain( + 'Header "host" contains characters that are not allowed.', + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('Header "host" contains characters that are not allowed.'), + ); + }); + }); + + describe('without allowed hosts configured', () => { + beforeAll(() => { + setAngularAppEngineManifest({ + allowedHosts: [], + entryPoints: { + '': async () => { + setAngularAppTestingManifest( + [{ path: 'home', component: TestHomeComponent }], + [{ path: '**', renderMode: RenderMode.Server }], + ); + + return { + ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, + ɵdestroyAngularServerApp: destroyAngularServerApp, + }; + }, + }, + basePath: '/', + supportedLocales: { 'en-US': '' }, + }); + + appEngine = new AngularAppEngine(); + }); + + beforeEach(() => { + consoleErrorSpy = spyOn(console, 'error'); + }); + + it('should log error and fallback to CSR when disallowed host', async () => { + const request = new Request('https://example.com'); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(await response?.text()).toContain('CSR page'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('URL with hostname "example.com" is not allowed.'), + ); + }); + + it('should log error and fallback to CSR when host with path separator', async () => { + const request = new Request('https://example.com/home', { + headers: { 'host': 'example.com/evil' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(await response?.text()).toContain('CSR page'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('Header "host" contains characters that are not allowed.'), + ); + }); + }); + }); }); diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts new file mode 100644 index 000000000000..99fbf517b60d --- /dev/null +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -0,0 +1,240 @@ +/** + * @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 { + cloneRequestAndPatchHeaders, + getFirstHeaderValue, + validateRequest, + validateUrl, +} from '../../src/utils/validation'; + +describe('Validation Utils', () => { + describe('getFirstHeaderValue', () => { + it('should return the first value from a comma-separated string', () => { + expect(getFirstHeaderValue('value1, value2')).toBe('value1'); + }); + + it('should return the value if it is a single string', () => { + expect(getFirstHeaderValue('value1')).toBe('value1'); + }); + + it('should return the first value from an array of strings', () => { + expect(getFirstHeaderValue(['value1', 'value2'])).toBe('value1'); + }); + + it('should return undefined for null or undefined', () => { + expect(getFirstHeaderValue(null)).toBeUndefined(); + expect(getFirstHeaderValue(undefined)).toBeUndefined(); + }); + + it('should return empty string for empty string input', () => { + expect(getFirstHeaderValue('')).toBe(''); + }); + }); + + describe('validateUrl', () => { + const allowedHosts = new Set(['example.com', '*.google.com']); + + it('should pass for allowed hostname', () => { + expect(() => validateUrl(new URL('http://example.com'), allowedHosts)).not.toThrow(); + }); + + it('should pass for wildcard allowed hostname', () => { + expect(() => validateUrl(new URL('http://foo.google.com'), allowedHosts)).not.toThrow(); + }); + + it('should throw for disallowed hostname', () => { + expect(() => validateUrl(new URL('http://evil.com'), allowedHosts)).toThrowError( + /URL with hostname "evil.com" is not allowed/, + ); + }); + + it('should match subdomains for wildcard', () => { + expect(() => validateUrl(new URL('http://sub.foo.google.com'), allowedHosts)).not.toThrow(); + }); + + it('should not match base domain for wildcard (*.google.com vs google.com)', () => { + // Logic: hostname.endsWith('.google.com') -> 'google.com'.endsWith('.google.com') is false + expect(() => validateUrl(new URL('http://google.com'), allowedHosts)).toThrowError( + /URL with hostname "google.com" is not allowed/, + ); + }); + }); + + describe('validateRequest', () => { + const allowedHosts = new Set(['example.com']); + + it('should pass for valid request', () => { + const req = new Request('http://example.com', { + headers: { + 'x-forwarded-port': '443', + 'x-forwarded-proto': 'https', + }, + }); + + expect(() => validateRequest(req, allowedHosts)).not.toThrow(); + }); + + it('should throw if URL hostname is invalid', () => { + const req = new Request('http://evil.com'); + + expect(() => validateRequest(req, allowedHosts)).toThrowError( + /URL with hostname "evil.com" is not allowed/, + ); + }); + + it('should throw if x-forwarded-port is invalid', () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-port': 'abc' }, + }); + + expect(() => validateRequest(req, allowedHosts)).toThrowError( + 'Header "x-forwarded-port" must be a numeric value.', + ); + }); + + it('should throw if x-forwarded-proto is invalid', () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-proto': 'ftp' }, + }); + expect(() => validateRequest(req, allowedHosts)).toThrowError( + 'Header "x-forwarded-proto" must be either "http" or "https".', + ); + }); + + it('should throw if host contains path separators', () => { + const req = new Request('http://example.com', { + headers: { 'host': 'example.com/bad' }, + }); + expect(() => validateRequest(req, allowedHosts)).toThrowError( + 'Header "host" contains characters that are not allowed.', + ); + }); + + it('should throw if x-forwarded-host contains path separators', () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-host': 'example.com/bad' }, + }); + expect(() => validateRequest(req, allowedHosts)).toThrowError( + 'Header "x-forwarded-host" contains characters that are not allowed.', + ); + }); + }); + + describe('cloneRequestAndPatchHeaders', () => { + const allowedHosts = new Set(['example.com', '*.valid.com']); + + it('should validate host header when accessed via get()', async () => { + const req = new Request('http://example.com', { + headers: { 'host': 'evil.com' }, + }); + const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); + + expect(() => secured.headers.get('host')).toThrowError( + 'Header "host" with value "evil.com" is not allowed.', + ); + await expectAsync(onError).toBeResolvedTo( + jasmine.objectContaining({ + message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed'), + }), + ); + }); + + it('should allow valid host header', () => { + const req = new Request('http://example.com', { + headers: { 'host': 'example.com' }, + }); + const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts); + expect(secured.headers.get('host')).toBe('example.com'); + }); + + it('should validate x-forwarded-host header', async () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-host': 'evil.com' }, + }); + const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); + + expect(() => secured.headers.get('x-forwarded-host')).toThrowError( + 'Header "x-forwarded-host" with value "evil.com" is not allowed.', + ); + await expectAsync(onError).toBeResolvedTo( + jasmine.objectContaining({ + message: jasmine.stringMatching( + 'Header "x-forwarded-host" with value "evil.com" is not allowed', + ), + }), + ); + }); + + it('should allow accessing other headers without validation', () => { + const req = new Request('http://example.com', { + headers: { 'accept': 'application/json' }, + }); + const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts); + + expect(secured.headers.get('accept')).toBe('application/json'); + }); + + it('should validate headers when iterating with entries()', async () => { + const req = new Request('http://example.com', { + headers: { 'host': 'evil.com' }, + }); + const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); + + expect(() => { + for (const _ of secured.headers.entries()) { + // access the header to trigger the validation + } + }).toThrowError('Header "host" with value "evil.com" is not allowed.'); + + await expectAsync(onError).toBeResolvedTo( + jasmine.objectContaining({ + message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), + }), + ); + }); + + it('should validate headers when iterating with values()', async () => { + const req = new Request('http://example.com', { + headers: { 'host': 'evil.com' }, + }); + const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); + + expect(() => { + for (const _ of secured.headers.values()) { + // access the header to trigger the validation + } + }).toThrowError('Header "host" with value "evil.com" is not allowed.'); + + await expectAsync(onError).toBeResolvedTo( + jasmine.objectContaining({ + message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), + }), + ); + }); + + it('should validate headers when iterating with for...of', async () => { + const req = new Request('http://example.com', { + headers: { 'host': 'evil.com' }, + }); + const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); + + expect(() => { + for (const _ of secured.headers) { + // access the header to trigger the validation + } + }).toThrowError('Header "host" with value "evil.com" is not allowed.'); + + await expectAsync(onError).toBeResolvedTo( + jasmine.objectContaining({ + message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), + }), + ); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts index 6219ea6a46a2..37e04171aed4 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts @@ -114,7 +114,13 @@ export function execute( return of([b, s]); } - return startNodeServer(s, nodeServerPort, context.logger, !!options.inspect).pipe( + return startNodeServer( + s, + nodeServerPort, + options.host, + context.logger, + !!options.inspect, + ).pipe( map(() => [b, s]), catchError((err) => { context.logger.error(`A server error has occurred.\n${mapErrorToMessage(err)}`); @@ -216,12 +222,13 @@ export function log( function startNodeServer( serverOutput: BuilderOutput, port: number, + host: string | undefined, logger: logging.LoggerApi, inspectMode = false, ): Observable { const outputPath = serverOutput.outputPath as string; const path = join(outputPath, 'main.js'); - const env = { ...process.env, PORT: '' + port }; + const env = { ...process.env, PORT: '' + port, NG_ALLOWED_HOSTS: host ?? 'localhost' }; const args = ['--enable-source-maps', `"${path}"`]; if (inspectMode) { diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts index a4cafd66ee06..b125312653e9 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts @@ -54,11 +54,12 @@ describe('Serve SSR Builder', () => { })); server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap: AppServerModule, documentFilePath: indexHtml, - url: req.originalUrl, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, publicPath: distFolder, }) .then((html) => res.send(html)) diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts index 3792d87f839c..4a358ee1c123 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts @@ -54,11 +54,12 @@ describe('Serve SSR Builder', () => { })); server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap: AppServerModule, documentFilePath: indexHtml, - url: req.originalUrl, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, publicPath: distFolder, }) .then((html) => res.send(html)) diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts index 8e92c4d666e3..3feed5d3161a 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts @@ -53,11 +53,12 @@ describe('Serve SSR Builder', () => { })); server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap: AppServerModule, documentFilePath: indexHtml, - url: req.originalUrl, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, publicPath: distFolder, }) .then((html) => res.send(html)) diff --git a/packages/schematics/angular/ssr/files/server-builder/server.ts.template b/packages/schematics/angular/ssr/files/server-builder/server.ts.template index 7567fa65a81d..92272edc590d 100644 --- a/packages/schematics/angular/ssr/files/server-builder/server.ts.template +++ b/packages/schematics/angular/ssr/files/server-builder/server.ts.template @@ -15,7 +15,9 @@ export function app(): express.Express { ? join(distFolder, 'index.original.html') : join(distFolder, 'index.html'); - const commonEngine = new CommonEngine(); + const commonEngine = new CommonEngine({ + allowedHosts: [/* Configure your hosts here */] + }); server.set('view engine', 'html'); server.set('views', distFolder); diff --git a/packages/schematics/angular/ssr/index.ts b/packages/schematics/angular/ssr/index.ts index e589395dac73..90744240e43b 100644 --- a/packages/schematics/angular/ssr/index.ts +++ b/packages/schematics/angular/ssr/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { isJsonObject, join, normalize, strings } from '@angular-devkit/core'; +import { JsonObject, isJsonObject, join, normalize, strings } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -204,6 +204,10 @@ function updateApplicationBuilderWorkspaceConfigRule( buildTarget.options = { ...buildTarget.options, + security: { + ...((buildTarget.options?.security as JsonObject | undefined) ?? {}), + allowedHosts: [], + }, outputPath, outputMode: 'server', ssr: { diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts index 19e7dcd28b60..22befac99f9b 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts @@ -144,6 +144,7 @@ export default async function () { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts index f05d2182bbd2..f21690c47df9 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts @@ -157,6 +157,7 @@ export default async function () { /Node Express server listening on/, { ...process.env, + 'NG_ALLOWED_HOSTS': 'localhost', 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts index 7c819e67693a..b8df3812f05c 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts @@ -114,6 +114,7 @@ export default async function () { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts index 6d0a45459a16..aa6f7e3426ee 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts @@ -111,6 +111,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts index 79fc755c4477..7bc323311b4f 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts @@ -146,6 +146,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts index 994d77343d1e..efbff9871bbc 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts @@ -122,6 +122,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts index 130ade10ba9f..6fdd1998cdd2 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts @@ -11,7 +11,6 @@ import { import { updateJsonFile, useSha } from '../../../utils/project'; import { getGlobalVariable } from '../../../utils/env'; import { findFreePort } from '../../../utils/network'; -import { readFile } from 'node:fs/promises'; export default async function () { assert( @@ -98,6 +97,8 @@ export default async function () { const options = buildTarget['options']; options['ssr']['experimentalPlatform'] = 'neutral'; options['outputMode'] = 'server'; + options['security'] ??= {}; + options['security']['allowedHosts'] = ['localhost']; }); await noSilentNg('build'); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts index 5205d20eeb0a..ff72cb8e8df2 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts @@ -206,6 +206,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts index f1437392492d..fe316e3cd157 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts @@ -196,6 +196,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); From 8700e18d7cf175d80fe6ce6205589767b7870c1c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:41:08 +0000 Subject: [PATCH 02/26] fix(@angular/ssr): prevent open redirect via X-Forwarded-Prefix header This change addresses a security vulnerability where `joinUrlParts()` in `packages/angular/ssr/src/utils/url.ts` only stripped one leading slash from URL parts. When the `X-Forwarded-Prefix` header contains multiple leading slashes (e.g., `///evil.com`), the function previously produced a protocol-relative URL (e.g., `//evil.com/home`). If the application issues a redirect (e.g., via a generic redirect route), the browser interprets this 'Location' header as an external redirect to `https://evil.com/home`. This vulnerability poses a significant risk as open redirects can be used in phishing attacks. Additionally, since the redirect response may lack `Cache-Control` headers, intermediate CDNs could cache the poisoned redirect, serving it to other users. This commit fixes the issue by: 1. Updating `joinUrlParts` to internally strip *all* leading and trailing slashes from URL segments, preventing the formation of protocol-relative URLs from malicious input. 2. Adding strict validation for the `X-Forwarded-Prefix` header to immediately reject requests with values starting with multiple slashest pusfh: (`//`) or backslashes (`\\`). Closes #32501 --- packages/angular/ssr/src/utils/url.ts | 24 ++++--- packages/angular/ssr/src/utils/validation.ts | 12 ++++ packages/angular/ssr/test/utils/url_spec.ts | 12 ++++ .../angular/ssr/test/utils/validation_spec.ts | 67 +++++++++++++++++++ 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/packages/angular/ssr/src/utils/url.ts b/packages/angular/ssr/src/utils/url.ts index faabf15b1bd4..caf7f3aad310 100644 --- a/packages/angular/ssr/src/utils/url.ts +++ b/packages/angular/ssr/src/utils/url.ts @@ -95,26 +95,32 @@ export function addTrailingSlash(url: string): string { * ``` */ export function joinUrlParts(...parts: string[]): string { - const normalizeParts: string[] = []; + const normalizedParts: string[] = []; + for (const part of parts) { if (part === '') { // Skip any empty parts continue; } - let normalizedPart = part; - if (part[0] === '/') { - normalizedPart = normalizedPart.slice(1); + let start = 0; + let end = part.length; + + // Use "Pointers" to avoid intermediate slices + while (start < end && part[start] === '/') { + start++; } - if (part[part.length - 1] === '/') { - normalizedPart = normalizedPart.slice(0, -1); + + while (end > start && part[end - 1] === '/') { + end--; } - if (normalizedPart !== '') { - normalizeParts.push(normalizedPart); + + if (start < end) { + normalizedParts.push(part.slice(start, end)); } } - return addLeadingSlash(normalizeParts.join('/')); + return addLeadingSlash(normalizedParts.join('/')); } /** diff --git a/packages/angular/ssr/src/utils/validation.ts b/packages/angular/ssr/src/utils/validation.ts index 97ac8606f3b4..c89cdd6a64ed 100644 --- a/packages/angular/ssr/src/utils/validation.ts +++ b/packages/angular/ssr/src/utils/validation.ts @@ -26,6 +26,11 @@ const VALID_PROTO_REGEX = /^https?$/i; */ const VALID_HOST_REGEX = /^[a-z0-9.:-]+$/i; +/** + * Regular expression to validate that the prefix is valid. + */ +const INVALID_PREFIX_REGEX = /^[/\\]{2}|(?:^|[/\\])\.\.?(?:[/\\]|$)/; + /** * Extracts the first value from a multi-value header string. * @@ -253,4 +258,11 @@ function validateHeaders(request: Request): void { if (xForwardedProto && !VALID_PROTO_REGEX.test(xForwardedProto)) { throw new Error('Header "x-forwarded-proto" must be either "http" or "https".'); } + + const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix')); + if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) { + throw new Error( + 'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.', + ); + } } diff --git a/packages/angular/ssr/test/utils/url_spec.ts b/packages/angular/ssr/test/utils/url_spec.ts index 9a7a7cb3ad49..a108c7ff1df6 100644 --- a/packages/angular/ssr/test/utils/url_spec.ts +++ b/packages/angular/ssr/test/utils/url_spec.ts @@ -100,6 +100,18 @@ describe('URL Utils', () => { it('should handle an all-empty URL parts', () => { expect(joinUrlParts('', '')).toBe('/'); }); + + it('should normalize parts with multiple leading and trailing slashes', () => { + expect(joinUrlParts('//path//', '///to///', '//resource//')).toBe('/path/to/resource'); + }); + + it('should handle a single part', () => { + expect(joinUrlParts('path')).toBe('/path'); + }); + + it('should handle parts containing only slashes', () => { + expect(joinUrlParts('//', '///')).toBe('/'); + }); }); describe('stripIndexHtmlFromURL', () => { diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts index 99fbf517b60d..10ab896e36f1 100644 --- a/packages/angular/ssr/test/utils/validation_spec.ts +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -124,6 +124,73 @@ describe('Validation Utils', () => { 'Header "x-forwarded-host" contains characters that are not allowed.', ); }); + + it('should throw error if x-forwarded-prefix starts with multiple slashes or backslashes', () => { + const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil']; + + for (const prefix of inputs) { + const request = new Request('https://example.com', { + headers: { + 'x-forwarded-prefix': prefix, + }, + }); + + expect(() => validateRequest(request, allowedHosts)) + .withContext(`Prefix: "${prefix}"`) + .toThrowError( + 'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.', + ); + } + }); + + it('should throw error if x-forwarded-prefix contains dot segments', () => { + const inputs = [ + '/./', + '/../', + '/foo/./bar', + '/foo/../bar', + '/.', + '/..', + './', + '../', + '.\\', + '..\\', + '/foo/.\\bar', + '/foo/..\\bar', + '.', + '..', + ]; + + for (const prefix of inputs) { + const request = new Request('https://example.com', { + headers: { + 'x-forwarded-prefix': prefix, + }, + }); + + expect(() => validateRequest(request, allowedHosts)) + .withContext(`Prefix: "${prefix}"`) + .toThrowError( + 'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.', + ); + } + }); + + it('should validate x-forwarded-prefix with valid dot usage', () => { + const inputs = ['/foo.bar', '/foo.bar/baz', '/v1.2', '/.well-known']; + + for (const prefix of inputs) { + const request = new Request('https://example.com', { + headers: { + 'x-forwarded-prefix': prefix, + }, + }); + + expect(() => validateRequest(request, allowedHosts)) + .withContext(`Prefix: "${prefix}"`) + .not.toThrow(); + } + }); }); describe('cloneRequestAndPatchHeaders', () => { From c0d1626e87b807845d898e30f52e2251aa81c2a0 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:09:56 +0000 Subject: [PATCH 03/26] release: cut the v20.3.17 release --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71eb5de722cd..7e76df245531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ + + +# 20.3.17 (2026-02-23) + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [8700e18d7](https://github.com/angular/angular-cli/commit/8700e18d7cf175d80fe6ce6205589767b7870c1c) | fix | prevent open redirect via X-Forwarded-Prefix header | +| [67582a946](https://github.com/angular/angular-cli/commit/67582a946808d2c021cbcfacbf203ef58a6fbded) | fix | validate host headers to prevent header-based SSRF | + + + # 20.3.16 (2026-02-09) diff --git a/package.json b/package.json index b76fb7e75829..558bedcec040 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.16", + "version": "20.3.17", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From f668e2778c4c4dbecc8a1c6831c092f5512d1ec1 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:05:34 +0000 Subject: [PATCH 04/26] fix(@angular/build): update rollup to 4.59.0 This fixes GHSA-mw96-cpmx-2vgc Closes #32592 --- package.json | 2 +- packages/angular/build/package.json | 2 +- pnpm-lock.yaml | 487 +++++++++------------------- 3 files changed, 148 insertions(+), 343 deletions(-) diff --git a/package.json b/package.json index 558bedcec040..05e6c9c80384 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "protractor": "~7.0.0", "puppeteer": "18.2.1", "quicktype-core": "23.2.6", - "rollup": "4.46.2", + "rollup": "4.59.0", "rollup-license-plugin": "~3.0.1", "semver": "7.7.2", "shelljs": "^0.10.0", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index ea57319b5721..2a3a98d341da 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -37,7 +37,7 @@ "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.3", "piscina": "5.1.3", - "rollup": "4.52.3", + "rollup": "4.59.0", "sass": "1.90.0", "semver": "7.7.2", "source-map-support": "0.5.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80137681a0e6..b9cf605c93f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,16 +78,16 @@ importers: version: 9.33.0 '@rollup/plugin-alias': specifier: ^5.1.1 - version: 5.1.1(rollup@4.46.2) + version: 5.1.1(rollup@4.59.0) '@rollup/plugin-commonjs': specifier: ^28.0.0 - version: 28.0.6(rollup@4.46.2) + version: 28.0.6(rollup@4.59.0) '@rollup/plugin-json': specifier: ^6.1.0 - version: 6.1.0(rollup@4.46.2) + version: 6.1.0(rollup@4.59.0) '@rollup/plugin-node-resolve': specifier: 16.0.1 - version: 16.0.1(rollup@4.46.2) + version: 16.0.1(rollup@4.59.0) '@stylistic/eslint-plugin': specifier: ^5.0.0 version: 5.4.0(eslint@9.33.0(jiti@1.21.7)) @@ -272,17 +272,17 @@ importers: specifier: 23.2.6 version: 23.2.6(encoding@0.1.13) rollup: - specifier: 4.46.2 - version: 4.46.2 + specifier: 4.59.0 + version: 4.59.0 rollup-license-plugin: specifier: ~3.0.1 version: 3.0.2 rollup-plugin-dts: specifier: 6.2.1 - version: 6.2.1(rollup@4.46.2)(typescript@5.9.2) + version: 6.2.1(rollup@4.59.0)(typescript@5.9.2) rollup-plugin-sourcemaps2: specifier: 0.5.3 - version: 0.5.3(@types/node@22.18.10)(rollup@4.46.2) + version: 0.5.3(@types/node@22.18.10)(rollup@4.59.0) semver: specifier: 7.7.2 version: 7.7.2 @@ -404,8 +404,8 @@ importers: specifier: 5.1.3 version: 5.1.3 rollup: - specifier: 4.52.3 - version: 4.52.3 + specifier: 4.59.0 + version: 4.59.0 sass: specifier: 1.90.0 version: 1.90.0 @@ -3071,235 +3071,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.46.2': - resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm-eabi@4.52.3': - resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.46.2': - resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-android-arm64@4.52.3': - resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.46.2': - resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-arm64@4.52.3': - resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.46.2': - resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.52.3': - resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.2': - resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.52.3': - resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.46.2': - resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.3': - resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': - resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm-musleabihf@4.52.3': - resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.46.2': - resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-gnu@4.52.3': - resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.46.2': - resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-musl@4.52.3': - resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.52.3': - resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [glibc] + libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.52.3': - resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} - cpu: [riscv64] - os: [linux] - libc: [glibc] + libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.52.3': - resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.46.2': - resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-musl@4.52.3': - resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.46.2': - resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-s390x-gnu@4.52.3': - resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.46.2': - resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.52.3': - resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.46.2': - resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-linux-x64-musl@4.52.3': - resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} cpu: [x64] - os: [linux] - libc: [musl] + os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.52.3': - resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.46.2': - resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.52.3': - resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.46.2': - resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.3': - resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.52.3': - resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.46.2': - resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.3': - resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} cpu: [x64] os: [win32] @@ -7870,13 +7776,8 @@ packages: '@types/node': optional: true - rollup@4.46.2: - resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rollup@4.52.3: - resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -11645,13 +11546,13 @@ snapshots: - react-native-b4a - supports-color - '@rollup/plugin-alias@5.1.1(rollup@4.46.2)': + '@rollup/plugin-alias@5.1.1(rollup@4.59.0)': optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/plugin-commonjs@28.0.6(rollup@4.46.2)': + '@rollup/plugin-commonjs@28.0.6(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.46.2) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.3) @@ -11659,188 +11560,123 @@ snapshots: magic-string: 0.30.17 picomatch: 4.0.3 optionalDependencies: - rollup: 4.46.2 - - '@rollup/plugin-json@6.1.0(rollup@4.46.2)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.46.2) - optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/plugin-json@6.1.0(rollup@4.52.3)': + '@rollup/plugin-json@6.1.0(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.3) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) optionalDependencies: - rollup: 4.52.3 + rollup: 4.59.0 - '@rollup/plugin-node-resolve@15.3.1(rollup@4.52.3)': + '@rollup/plugin-node-resolve@15.3.1(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.3) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 4.52.3 + rollup: 4.59.0 - '@rollup/plugin-node-resolve@16.0.1(rollup@4.46.2)': + '@rollup/plugin-node-resolve@16.0.1(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.46.2) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 4.46.2 - - '@rollup/pluginutils@5.2.0(rollup@4.46.2)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/pluginutils@5.3.0(rollup@4.46.2)': + '@rollup/pluginutils@5.2.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/pluginutils@5.3.0(rollup@4.52.3)': + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.52.3 + rollup: 4.59.0 - '@rollup/rollup-android-arm-eabi@4.46.2': + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true - '@rollup/rollup-android-arm-eabi@4.52.3': + '@rollup/rollup-android-arm64@4.59.0': optional: true - '@rollup/rollup-android-arm64@4.46.2': + '@rollup/rollup-darwin-arm64@4.59.0': optional: true - '@rollup/rollup-android-arm64@4.52.3': + '@rollup/rollup-darwin-x64@4.59.0': optional: true - '@rollup/rollup-darwin-arm64@4.46.2': + '@rollup/rollup-freebsd-arm64@4.59.0': optional: true - '@rollup/rollup-darwin-arm64@4.52.3': + '@rollup/rollup-freebsd-x64@4.59.0': optional: true - '@rollup/rollup-darwin-x64@4.46.2': + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': optional: true - '@rollup/rollup-darwin-x64@4.52.3': + '@rollup/rollup-linux-arm-musleabihf@4.59.0': optional: true - '@rollup/rollup-freebsd-arm64@4.46.2': + '@rollup/rollup-linux-arm64-gnu@4.59.0': optional: true - '@rollup/rollup-freebsd-arm64@4.52.3': + '@rollup/rollup-linux-arm64-musl@4.59.0': optional: true - '@rollup/rollup-freebsd-x64@4.46.2': + '@rollup/rollup-linux-loong64-gnu@4.59.0': optional: true - '@rollup/rollup-freebsd-x64@4.52.3': + '@rollup/rollup-linux-loong64-musl@4.59.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + '@rollup/rollup-linux-ppc64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + '@rollup/rollup-linux-ppc64-musl@4.59.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.2': + '@rollup/rollup-linux-riscv64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.3': + '@rollup/rollup-linux-riscv64-musl@4.59.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.2': + '@rollup/rollup-linux-s390x-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.3': + '@rollup/rollup-linux-x64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.2': + '@rollup/rollup-linux-x64-musl@4.59.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.3': + '@rollup/rollup-openbsd-x64@4.59.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.3': + '@rollup/rollup-openharmony-arm64@4.59.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + '@rollup/rollup-win32-arm64-msvc@4.59.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.2': + '@rollup/rollup-win32-ia32-msvc@4.59.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.3': + '@rollup/rollup-win32-x64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.52.3': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.46.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.52.3': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.52.3': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.46.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.52.3': - optional: true - - '@rollup/rollup-linux-x64-musl@4.46.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.52.3': - optional: true - - '@rollup/rollup-openharmony-arm64@4.52.3': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.46.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.52.3': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.46.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.52.3': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.52.3': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.46.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.52.3': + '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true '@rollup/wasm-node@4.52.4': @@ -12613,11 +12449,11 @@ snapshots: '@web/dev-server-rollup@0.6.4(bufferutil@4.0.9)': dependencies: - '@rollup/plugin-node-resolve': 15.3.1(rollup@4.52.3) + '@rollup/plugin-node-resolve': 15.3.1(rollup@4.59.0) '@web/dev-server-core': 0.7.5(bufferutil@4.0.9) nanocolors: 0.2.13 parse5: 6.0.1 - rollup: 4.52.3 + rollup: 4.59.0 whatwg-url: 14.2.0 transitivePeerDependencies: - bufferutil @@ -16303,7 +16139,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular/compiler-cli': 20.3.7(@angular/compiler@20.3.7)(typescript@5.9.2) - '@rollup/plugin-json': 6.1.0(rollup@4.52.3) + '@rollup/plugin-json': 6.1.0(rollup@4.59.0) '@rollup/wasm-node': 4.52.4 ajv: 8.17.1 ansi-colors: 4.1.3 @@ -16319,14 +16155,14 @@ snapshots: ora: 8.2.0 piscina: 5.1.3 postcss: 8.5.6 - rollup-plugin-dts: 6.2.1(rollup@4.52.3)(typescript@5.9.2) + rollup-plugin-dts: 6.2.1(rollup@4.59.0)(typescript@5.9.2) rxjs: 7.8.2 sass: 1.90.0 tinyglobby: 0.2.14 tslib: 2.8.1 typescript: 5.9.2 optionalDependencies: - rollup: 4.52.3 + rollup: 4.59.0 nock@14.0.10: dependencies: @@ -17257,81 +17093,50 @@ snapshots: node-fetch: 3.3.2 spdx-expression-validate: 2.0.0 - rollup-plugin-dts@6.2.1(rollup@4.46.2)(typescript@5.9.2): + rollup-plugin-dts@6.2.1(rollup@4.59.0)(typescript@5.9.2): dependencies: magic-string: 0.30.17 - rollup: 4.46.2 + rollup: 4.59.0 typescript: 5.9.2 optionalDependencies: '@babel/code-frame': 7.27.1 - rollup-plugin-dts@6.2.1(rollup@4.52.3)(typescript@5.9.2): + rollup-plugin-sourcemaps2@0.5.3(@types/node@22.18.10)(rollup@4.59.0): dependencies: - magic-string: 0.30.17 - rollup: 4.52.3 - typescript: 5.9.2 - optionalDependencies: - '@babel/code-frame': 7.27.1 - - rollup-plugin-sourcemaps2@0.5.3(@types/node@22.18.10)(rollup@4.46.2): - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) - rollup: 4.46.2 + '@rollup/pluginutils': 5.2.0(rollup@4.59.0) + rollup: 4.59.0 optionalDependencies: '@types/node': 22.18.10 - rollup@4.46.2: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.2 - '@rollup/rollup-android-arm64': 4.46.2 - '@rollup/rollup-darwin-arm64': 4.46.2 - '@rollup/rollup-darwin-x64': 4.46.2 - '@rollup/rollup-freebsd-arm64': 4.46.2 - '@rollup/rollup-freebsd-x64': 4.46.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 - '@rollup/rollup-linux-arm-musleabihf': 4.46.2 - '@rollup/rollup-linux-arm64-gnu': 4.46.2 - '@rollup/rollup-linux-arm64-musl': 4.46.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 - '@rollup/rollup-linux-ppc64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-musl': 4.46.2 - '@rollup/rollup-linux-s390x-gnu': 4.46.2 - '@rollup/rollup-linux-x64-gnu': 4.46.2 - '@rollup/rollup-linux-x64-musl': 4.46.2 - '@rollup/rollup-win32-arm64-msvc': 4.46.2 - '@rollup/rollup-win32-ia32-msvc': 4.46.2 - '@rollup/rollup-win32-x64-msvc': 4.46.2 - fsevents: 2.3.3 - - rollup@4.52.3: + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.3 - '@rollup/rollup-android-arm64': 4.52.3 - '@rollup/rollup-darwin-arm64': 4.52.3 - '@rollup/rollup-darwin-x64': 4.52.3 - '@rollup/rollup-freebsd-arm64': 4.52.3 - '@rollup/rollup-freebsd-x64': 4.52.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 - '@rollup/rollup-linux-arm-musleabihf': 4.52.3 - '@rollup/rollup-linux-arm64-gnu': 4.52.3 - '@rollup/rollup-linux-arm64-musl': 4.52.3 - '@rollup/rollup-linux-loong64-gnu': 4.52.3 - '@rollup/rollup-linux-ppc64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-musl': 4.52.3 - '@rollup/rollup-linux-s390x-gnu': 4.52.3 - '@rollup/rollup-linux-x64-gnu': 4.52.3 - '@rollup/rollup-linux-x64-musl': 4.52.3 - '@rollup/rollup-openharmony-arm64': 4.52.3 - '@rollup/rollup-win32-arm64-msvc': 4.52.3 - '@rollup/rollup-win32-ia32-msvc': 4.52.3 - '@rollup/rollup-win32-x64-gnu': 4.52.3 - '@rollup/rollup-win32-x64-msvc': 4.52.3 + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 router@2.2.0: @@ -18470,7 +18275,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.3 + rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.9.1 @@ -18488,7 +18293,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.3 + rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.9.1 From 39596d529f831f72a2134bc3c9ac163867ff5702 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:06:32 +0000 Subject: [PATCH 05/26] fix(@angular-devkit/core): update `ajv` to `8.18.0` This fixes GHSA-2g4f-4pwh-qvx6 Closes #32592 --- package.json | 2 +- packages/angular_devkit/core/package.json | 2 +- pnpm-lock.yaml | 34 ++++++++++++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 05e6c9c80384..26f7b8512c1e 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@types/yarnpkg__lockfile": "^1.1.5", "@typescript-eslint/eslint-plugin": "8.39.1", "@typescript-eslint/parser": "8.39.1", - "ajv": "8.17.1", + "ajv": "8.18.0", "ansi-colors": "4.1.3", "beasties": "0.3.5", "buffer": "6.0.3", diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index 2fa6f3eb24a4..e2daae128bdc 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -25,7 +25,7 @@ "./*.js": "./*.js" }, "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9cf605c93f6..da74e29c83af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,8 +170,8 @@ importers: specifier: 8.39.1 version: 8.39.1(eslint@9.33.0(jiti@1.21.7))(typescript@5.9.2) ajv: - specifier: 8.17.1 - version: 8.17.1 + specifier: 8.18.0 + version: 8.18.0 ansi-colors: specifier: 4.1.3 version: 4.1.3 @@ -795,11 +795,11 @@ importers: packages/angular_devkit/core: dependencies: ajv: - specifier: 8.17.1 - version: 8.17.1 + specifier: 8.18.0 + version: 8.18.0 ajv-formats: specifier: 3.0.1 - version: 3.0.1(ajv@8.17.1) + version: 3.0.1(ajv@8.18.0) jsonc-parser: specifier: 3.3.1 version: 3.3.1 @@ -3938,6 +3938,9 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + algoliasearch@5.35.0: resolution: {integrity: sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==} engines: {node: '>= 14.0.0'} @@ -12728,15 +12731,19 @@ snapshots: ajv-formats@2.1.1: dependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 + ajv: 8.18.0 fast-deep-equal: 3.1.3 ajv@6.12.6: @@ -12753,6 +12760,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch@5.35.0: dependencies: '@algolia/abtesting': 1.1.0 @@ -17220,9 +17234,9 @@ snapshots: schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats: 2.1.1 - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.18.0) select-hose@2.0.0: {} From 05b35113e680341f9f9465b0f35d93fd663ba59f Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Thu, 26 Feb 2026 12:47:06 -0800 Subject: [PATCH 06/26] release: cut the v20.3.18 release --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e76df245531..f8d22c867ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + + +# 20.3.18 (2026-02-26) + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | +| [39596d529](https://github.com/angular/angular-cli/commit/39596d529f831f72a2134bc3c9ac163867ff5702) | fix | update `ajv` to `8.18.0` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------- | +| [f668e2778](https://github.com/angular/angular-cli/commit/f668e2778c4c4dbecc8a1c6831c092f5512d1ec1) | fix | update rollup to 4.59.0 | + + + # 20.3.17 (2026-02-23) diff --git a/package.json b/package.json index 26f7b8512c1e..d751378daead 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.17", + "version": "20.3.18", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 0299b4d1aca13f11a06e2e92c593fe3e20906d23 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:25:15 -0500 Subject: [PATCH 07/26] fix(@angular-devkit/build-angular): update copy-webpack-plugin to v14.0.0 --- .../angular_devkit/build_angular/package.json | 2 +- pnpm-lock.yaml | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index e4253b78d121..0c9a4f13980d 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -26,7 +26,7 @@ "autoprefixer": "10.4.21", "babel-loader": "10.0.0", "browserslist": "^4.21.5", - "copy-webpack-plugin": "13.0.1", + "copy-webpack-plugin": "14.0.0", "css-loader": "7.1.2", "esbuild-wasm": "0.25.9", "fast-glob": "3.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da74e29c83af..7d23f2dbb743 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -648,8 +648,8 @@ importers: specifier: ^4.21.5 version: 4.26.3 copy-webpack-plugin: - specifier: 13.0.1 - version: 13.0.1(webpack@5.105.0(esbuild@0.25.9)) + specifier: 14.0.0 + version: 14.0.0(webpack@5.105.0(esbuild@0.25.9)) css-loader: specifier: 7.1.2 version: 7.1.2(webpack@5.105.0(esbuild@0.25.9)) @@ -4626,9 +4626,9 @@ packages: copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} - copy-webpack-plugin@13.0.1: - resolution: {integrity: sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==} - engines: {node: '>= 18.12.0'} + copy-webpack-plugin@14.0.0: + resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==} + engines: {node: '>= 20.9.0'} peerDependencies: webpack: ^5.1.0 @@ -7913,6 +7913,10 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-javascript@7.0.3: + resolution: {integrity: sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==} + engines: {node: '>=20.0.0'} + serve-index@1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} engines: {node: '>= 0.8.0'} @@ -13550,12 +13554,12 @@ snapshots: dependencies: is-what: 3.14.1 - copy-webpack-plugin@13.0.1(webpack@5.105.0(esbuild@0.25.9)): + copy-webpack-plugin@14.0.0(webpack@5.105.0(esbuild@0.25.9)): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 schema-utils: 4.3.3 - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.3 tinyglobby: 0.2.14 webpack: 5.105.0(esbuild@0.25.9) @@ -17316,6 +17320,8 @@ snapshots: dependencies: randombytes: 2.1.0 + serialize-javascript@7.0.3: {} + serve-index@1.9.1: dependencies: accepts: 1.3.8 From 93a6f3615f8e07fa7178ed1d6dc52eaa6b507ecd Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:40:48 -0500 Subject: [PATCH 08/26] release: cut the v20.3.19 release --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d22c867ae3..8fc145618dd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.3.19 (2026-03-04) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- | +| [0299b4d1a](https://github.com/angular/angular-cli/commit/0299b4d1aca13f11a06e2e92c593fe3e20906d23) | fix | update copy-webpack-plugin to v14.0.0 | + + + # 20.3.18 (2026-02-26) diff --git a/package.json b/package.json index d751378daead..a0387019ed7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.18", + "version": "20.3.19", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 0fd6823af0adec23f7c3f1d531f45f6432afe555 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:23:22 +0000 Subject: [PATCH 09/26] fix(@angular/build): pass process environment variables to prerender workers Worker processes used for prerendering and route extraction now inherit `process.env`. This ensures that any custom environment variables required by the application are available during the server-side rendering process. Closes #32730 --- packages/angular/build/src/utils/server-rendering/prerender.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index 39d0f0934c92..52890cac22ac 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -226,6 +226,7 @@ async function renderPages( } as RenderWorkerData, execArgv: workerExecArgv, env: { + ...process.env, 'NG_ALLOWED_HOSTS': 'localhost', }, }); @@ -341,6 +342,7 @@ async function getAllRoutes( } as RoutesExtractorWorkerData, execArgv: workerExecArgv, env: { + ...process.env, 'NG_ALLOWED_HOSTS': 'localhost', }, }); From 1e7d877d0a1c9f03bda8d7ec5411b45fffee114c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:56:04 +0000 Subject: [PATCH 10/26] release: cut the v20.3.20 release --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc145618dd9..dc083e4021f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.3.20 (2026-03-11) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [0fd6823af](https://github.com/angular/angular-cli/commit/0fd6823af0adec23f7c3f1d531f45f6432afe555) | fix | pass process environment variables to prerender workers | + + + # 20.3.19 (2026-03-04) diff --git a/package.json b/package.json index a0387019ed7e..dbc82633fb35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.19", + "version": "20.3.20", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 1dc6992a5ae6c5a1f16f22f6c94690d5cf218c38 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:23:43 +0000 Subject: [PATCH 11/26] fix(@angular/ssr): disallow x-forwarded-prefix starting with a backslash Updated the INVALID_PREFIX_REGEX to ensure that prefixes starting with a backslash are considered invalid. Previously, only multiple slashes or dot segments were explicitly disallowed at the start. Also updated the associated validation error message and unit tests to reflect this change. --- packages/angular/ssr/src/utils/validation.ts | 4 ++-- packages/angular/ssr/test/utils/validation_spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/angular/ssr/src/utils/validation.ts b/packages/angular/ssr/src/utils/validation.ts index c89cdd6a64ed..9e83e144b347 100644 --- a/packages/angular/ssr/src/utils/validation.ts +++ b/packages/angular/ssr/src/utils/validation.ts @@ -29,7 +29,7 @@ const VALID_HOST_REGEX = /^[a-z0-9.:-]+$/i; /** * Regular expression to validate that the prefix is valid. */ -const INVALID_PREFIX_REGEX = /^[/\\]{2}|(?:^|[/\\])\.\.?(?:[/\\]|$)/; +const INVALID_PREFIX_REGEX = /^(?:\\|\/[/\\])|(?:^|[/\\])\.\.?(?:[/\\]|$)/; /** * Extracts the first value from a multi-value header string. @@ -262,7 +262,7 @@ function validateHeaders(request: Request): void { const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix')); if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) { throw new Error( - 'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.', + 'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.', ); } } diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts index 10ab896e36f1..acf1e4829e8e 100644 --- a/packages/angular/ssr/test/utils/validation_spec.ts +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -125,8 +125,8 @@ describe('Validation Utils', () => { ); }); - it('should throw error if x-forwarded-prefix starts with multiple slashes or backslashes', () => { - const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil']; + it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes', () => { + const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil', '\\evil']; for (const prefix of inputs) { const request = new Request('https://example.com', { @@ -138,7 +138,7 @@ describe('Validation Utils', () => { expect(() => validateRequest(request, allowedHosts)) .withContext(`Prefix: "${prefix}"`) .toThrowError( - 'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.', + 'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.', ); } }); @@ -171,7 +171,7 @@ describe('Validation Utils', () => { expect(() => validateRequest(request, allowedHosts)) .withContext(`Prefix: "${prefix}"`) .toThrowError( - 'Header "x-forwarded-prefix" must not start with multiple "/" or "\\" or contain ".", ".." path segments.', + 'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.', ); } }); From cdbac82a85b35f24c70a062eeb8a13b521831019 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:09:28 +0000 Subject: [PATCH 12/26] fix(@angular/ssr): support custom headers in redirect responses Updates createRedirectResponse to accept an optional Record of headers, allowing custom headers to be merged into the redirect response. The Location and Vary: X-Forwarded-Prefix headers are automatically set to ensure correct routing and proxy behavior. AngularServerApp now passes relevant headers from the matched route or response context when creating a redirect. --- packages/angular/ssr/src/app.ts | 23 ++----- packages/angular/ssr/src/utils/redirect.ts | 66 +++++++++++++++++++ .../angular/ssr/test/utils/redirect_spec.ts | 60 +++++++++++++++++ 3 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 packages/angular/ssr/src/utils/redirect.ts create mode 100644 packages/angular/ssr/test/utils/redirect_spec.ts diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index 85227a9d8a33..d3e3b1dfa7bf 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -25,6 +25,7 @@ import { InlineCriticalCssProcessor } from './utils/inline-critical-css'; import { LRUCache } from './utils/lru-cache'; import { AngularBootstrap, renderAngular } from './utils/ng'; import { promiseWithAbort } from './utils/promise'; +import { createRedirectResponse } from './utils/redirect'; import { buildPathWithParams, joinUrlParts, stripLeadingSlash } from './utils/url'; /** @@ -174,7 +175,7 @@ export class AngularServerApp { return null; } - const { redirectTo, status, renderMode } = matchedRoute; + const { redirectTo, status, renderMode, headers } = matchedRoute; if (redirectTo !== undefined) { return createRedirectResponse( @@ -183,6 +184,7 @@ export class AngularServerApp { buildPathWithParams(redirectTo, url.pathname), ), status, + headers, ); } @@ -336,7 +338,7 @@ export class AngularServerApp { } if (result.redirectTo) { - return createRedirectResponse(result.redirectTo, status); + return createRedirectResponse(result.redirectTo, responseInit.status, headers); } if (renderMode === RenderMode.Prerender) { @@ -552,20 +554,3 @@ function appendPreloadHintsToHtml(html: string, preload: readonly string[]): str html.slice(bodyCloseIdx), ].join('\n'); } - -/** - * Creates an HTTP redirect response with a specified location and status code. - * - * @param location - The URL to which the response should redirect. - * @param status - The HTTP status code for the redirection. Defaults to 302 (Found). - * See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status - * @returns A `Response` object representing the HTTP redirect. - */ -function createRedirectResponse(location: string, status = 302): Response { - return new Response(null, { - status, - headers: { - 'Location': location, - }, - }); -} diff --git a/packages/angular/ssr/src/utils/redirect.ts b/packages/angular/ssr/src/utils/redirect.ts new file mode 100644 index 000000000000..3da6a0232bd8 --- /dev/null +++ b/packages/angular/ssr/src/utils/redirect.ts @@ -0,0 +1,66 @@ +/** + * @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 + */ + +/** + * An set of HTTP status codes that are considered valid for redirect responses. + */ +export const VALID_REDIRECT_RESPONSE_CODES: Set = new Set([301, 302, 303, 307, 308]); + +/** + * Checks if the given HTTP status code is a valid redirect response code. + * + * @param code The HTTP status code to check. + * @returns `true` if the code is a valid redirect response code, `false` otherwise. + */ +export function isValidRedirectResponseCode(code: number): boolean { + return VALID_REDIRECT_RESPONSE_CODES.has(code); +} + +/** + * Creates an HTTP redirect response with a specified location and status code. + * + * @param location - The URL to which the response should redirect. + * @param status - The HTTP status code for the redirection. Defaults to 302 (Found). + * See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status + * @param headers - Additional headers to include in the response. + * @returns A `Response` object representing the HTTP redirect. + */ +export function createRedirectResponse( + location: string, + status = 302, + headers?: Record, +): Response { + if (ngDevMode && !isValidRedirectResponseCode(status)) { + throw new Error( + `Invalid redirect status code: ${status}. ` + + `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`, + ); + } + + const resHeaders = new Headers(headers); + if (ngDevMode && resHeaders.has('location')) { + // eslint-disable-next-line no-console + console.warn( + `Location header "${resHeaders.get('location')}" will ignored and set to "${location}".`, + ); + } + + let vary = resHeaders.get('Vary') ?? ''; + if (vary) { + vary += ', '; + } + vary += 'X-Forwarded-Prefix'; + + resHeaders.set('Vary', vary); + resHeaders.set('Location', location); + + return new Response(null, { + status, + headers: resHeaders, + }); +} diff --git a/packages/angular/ssr/test/utils/redirect_spec.ts b/packages/angular/ssr/test/utils/redirect_spec.ts new file mode 100644 index 000000000000..bddbb81e2723 --- /dev/null +++ b/packages/angular/ssr/test/utils/redirect_spec.ts @@ -0,0 +1,60 @@ +/** + * @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 { createRedirectResponse } from '../../src/utils/redirect'; + +describe('Redirect Utils', () => { + describe('createRedirectResponse', () => { + it('should create a redirect response with default status 302', () => { + const response = createRedirectResponse('/home'); + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toBe('/home'); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix'); + }); + + it('should create a redirect response with a custom status', () => { + const response = createRedirectResponse('/home', 301); + expect(response.status).toBe(301); + expect(response.headers.get('Location')).toBe('/home'); + }); + + it('should allow providing additional headers', () => { + const response = createRedirectResponse('/home', 302, { 'X-Custom': 'value' }); + expect(response.headers.get('X-Custom')).toBe('value'); + expect(response.headers.get('Location')).toBe('/home'); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix'); + }); + + it('should append to Vary header instead of overriding it', () => { + const response = createRedirectResponse('/home', 302, { + 'Location': '/evil', + 'Vary': 'Host', + }); + expect(response.headers.get('Location')).toBe('/home'); + expect(response.headers.get('Vary')).toBe('Host, X-Forwarded-Prefix'); + }); + + it('should warn if Location header is provided in extra headers in dev mode', () => { + // @ts-expect-error accessing global + globalThis.ngDevMode = true; + const warnSpy = spyOn(console, 'warn'); + createRedirectResponse('/home', 302, { 'Location': '/evil' }); + expect(warnSpy).toHaveBeenCalledWith( + 'Location header "/evil" will ignored and set to "/home".', + ); + }); + + it('should throw error for invalid redirect status code in dev mode', () => { + // @ts-expect-error accessing global + globalThis.ngDevMode = true; + expect(() => createRedirectResponse('/home', 200)).toThrowError( + /Invalid redirect status code: 200/, + ); + }); + }); +}); From 0a2ff0b2b3aceb228c9447c19fb762df742d7265 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:20:09 +0000 Subject: [PATCH 13/26] fix(@angular/ssr): ensure unique values in redirect response Vary header Refactors the `createRedirectResponse` function to use a `Set` for constructing the `Vary` header. This ensures that `X-Forwarded-Prefix` is always present exactly once, and that existing `Vary` values from provided headers are correctly parsed, deduplicated, and preserved. Updates the associated unit tests to reflect the new header order and verify the deduplication logic. --- packages/angular/ssr/src/utils/redirect.ts | 15 ++++++++++----- packages/angular/ssr/test/utils/redirect_spec.ts | 9 ++++++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/angular/ssr/src/utils/redirect.ts b/packages/angular/ssr/src/utils/redirect.ts index 3da6a0232bd8..79fb10f424dc 100644 --- a/packages/angular/ssr/src/utils/redirect.ts +++ b/packages/angular/ssr/src/utils/redirect.ts @@ -50,13 +50,18 @@ export function createRedirectResponse( ); } - let vary = resHeaders.get('Vary') ?? ''; - if (vary) { - vary += ', '; + // Ensure unique values for Vary header + const varyArray = resHeaders.get('Vary')?.split(',') ?? []; + const varySet = new Set(['X-Forwarded-Prefix']); + for (const vary of varyArray) { + const value = vary.trim(); + + if (value) { + varySet.add(value); + } } - vary += 'X-Forwarded-Prefix'; - resHeaders.set('Vary', vary); + resHeaders.set('Vary', [...varySet].join(', ')); resHeaders.set('Location', location); return new Response(null, { diff --git a/packages/angular/ssr/test/utils/redirect_spec.ts b/packages/angular/ssr/test/utils/redirect_spec.ts index bddbb81e2723..b26edd458ac3 100644 --- a/packages/angular/ssr/test/utils/redirect_spec.ts +++ b/packages/angular/ssr/test/utils/redirect_spec.ts @@ -36,7 +36,14 @@ describe('Redirect Utils', () => { 'Vary': 'Host', }); expect(response.headers.get('Location')).toBe('/home'); - expect(response.headers.get('Vary')).toBe('Host, X-Forwarded-Prefix'); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix, Host'); + }); + + it('should NOT add duplicate X-Forwarded-Prefix if already present in Vary header', () => { + const response = createRedirectResponse('/home', 302, { + 'Vary': 'X-Forwarded-Prefix, Host', + }); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix, Host'); }); it('should warn if Location header is provided in extra headers in dev mode', () => { From 34d524549b68912f8ebe4e656a342b797161d232 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 19 Mar 2026 13:30:42 +0000 Subject: [PATCH 14/26] release: cut the v20.3.21 release --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc083e4021f7..1bf6e50b2a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + + +# 20.3.21 (2026-03-19) + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [1dc6992a5](https://github.com/angular/angular-cli/commit/1dc6992a5ae6c5a1f16f22f6c94690d5cf218c38) | fix | disallow x-forwarded-prefix starting with a backslash | +| [0a2ff0b2b](https://github.com/angular/angular-cli/commit/0a2ff0b2b3aceb228c9447c19fb762df742d7265) | fix | ensure unique values in redirect response Vary header | +| [cdbac82a8](https://github.com/angular/angular-cli/commit/cdbac82a85b35f24c70a062eeb8a13b521831019) | fix | support custom headers in redirect responses | + + + # 20.3.20 (2026-03-11) diff --git a/package.json b/package.json index dbc82633fb35..496d9947a0f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.20", + "version": "20.3.21", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 6f209c26dc5a454acd1cd76f25240c26978fa827 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:44:07 +0000 Subject: [PATCH 15/26] fix(@angular/build): update picomatch to 4.0.4 Update picomatch to address GHSA-c2c7-rcm5-vvqj --- packages/angular/build/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 2a3a98d341da..a381cc0b780e 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -35,7 +35,7 @@ "magic-string": "0.30.17", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "piscina": "5.1.3", "rollup": "4.59.0", "sass": "1.90.0", From 5978eeeff63cd62f1515d949eaad0b5e6f7c44cd Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:44:12 +0000 Subject: [PATCH 16/26] fix(@angular-devkit/build-angular): update picomatch to 4.0.4 Update picomatch to address GHSA-c2c7-rcm5-vvqj --- packages/angular_devkit/build_angular/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 0c9a4f13980d..1fc0a9d4bbdd 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -41,7 +41,7 @@ "mini-css-extract-plugin": "2.9.4", "open": "10.2.0", "ora": "8.2.0", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "piscina": "5.1.3", "postcss": "8.5.6", "postcss-loader": "8.1.1", From 6e9b926129a9dd79f01d47b7446411b8963ffb62 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:48:12 +0000 Subject: [PATCH 17/26] fix(@angular-devkit/core): update picomatch to 4.0.4 Update picomatch to address GHSA-c2c7-rcm5-vvqj --- packages/angular_devkit/core/package.json | 2 +- pnpm-lock.yaml | 48 +++++++++++++---------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index e2daae128bdc..b0fb4f9f0572 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -28,7 +28,7 @@ "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d23f2dbb743..e33567d0a696 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -398,8 +398,8 @@ importers: specifier: 8.0.0 version: 8.0.0 picomatch: - specifier: 4.0.3 - version: 4.0.3 + specifier: 4.0.4 + version: 4.0.4 piscina: specifier: 5.1.3 version: 5.1.3 @@ -693,8 +693,8 @@ importers: specifier: 8.2.0 version: 8.2.0 picomatch: - specifier: 4.0.3 - version: 4.0.3 + specifier: 4.0.4 + version: 4.0.4 piscina: specifier: 5.1.3 version: 5.1.3 @@ -804,8 +804,8 @@ importers: specifier: 3.3.1 version: 3.3.1 picomatch: - specifier: 4.0.3 - version: 4.0.3 + specifier: 4.0.4 + version: 4.0.4 rxjs: specifier: 7.8.2 version: 7.8.2 @@ -7349,6 +7349,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -11562,10 +11566,10 @@ snapshots: '@rollup/pluginutils': 5.3.0(rollup@4.59.0) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.4) is-reference: 1.2.1 magic-string: 0.30.17 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.59.0 @@ -11599,7 +11603,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.59.0 @@ -11607,7 +11611,7 @@ snapshots: dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: rollup: 4.59.0 @@ -11736,7 +11740,7 @@ snapshots: eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.3 + picomatch: 4.0.4 '@tootallnate/once@2.0.0': {} @@ -14444,9 +14448,9 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-blob@3.2.0: dependencies: @@ -16594,6 +16598,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pify@2.3.0: {} pify@3.0.0: {} @@ -17892,13 +17898,13 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinypool@1.1.1: {} @@ -18292,8 +18298,8 @@ snapshots: vite@7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 postcss: 8.5.6 rollup: 4.59.0 tinyglobby: 0.2.15 @@ -18310,8 +18316,8 @@ snapshots: vite@7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 postcss: 8.5.6 rollup: 4.59.0 tinyglobby: 0.2.15 From e18c1255e0d718a404bb9409fc07b478ce34d6bd Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:47:29 +0000 Subject: [PATCH 18/26] release: cut the v20.3.22 release --- CHANGELOG.md | 24 ++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf6e50b2a2a..9fb5cec8cc85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ + + +# 20.3.22 (2026-03-27) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [5978eeeff](https://github.com/angular/angular-cli/commit/5978eeeff63cd62f1515d949eaad0b5e6f7c44cd) | fix | update picomatch to 4.0.4 | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [6e9b92612](https://github.com/angular/angular-cli/commit/6e9b926129a9dd79f01d47b7446411b8963ffb62) | fix | update picomatch to 4.0.4 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [6f209c26d](https://github.com/angular/angular-cli/commit/6f209c26dc5a454acd1cd76f25240c26978fa827) | fix | update picomatch to 4.0.4 | + + + # 20.3.21 (2026-03-19) diff --git a/package.json b/package.json index 496d9947a0f4..00812901c745 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.21", + "version": "20.3.22", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From ccab02ba0413f25464a6e4cb5871716b221013b7 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 8 Apr 2026 06:53:00 +0000 Subject: [PATCH 19/26] fix(@angular/build): update vite to `7.3.2` This fixes GHSA-v2wj-q39q-566r --- packages/angular/build/package.json | 2 +- pnpm-lock.yaml | 354 ++++++++++++++++++++++------ 2 files changed, 283 insertions(+), 73 deletions(-) diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index a381cc0b780e..50d9ae359b89 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -42,7 +42,7 @@ "semver": "7.7.2", "source-map-support": "0.5.21", "tinyglobby": "0.2.14", - "vite": "7.1.11", + "vite": "7.3.2", "watchpack": "2.4.4" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e33567d0a696..d2d4169ee6e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -366,7 +366,7 @@ importers: version: 5.1.14(@types/node@24.9.1) '@vitejs/plugin-basic-ssl': specifier: 2.1.0 - version: 2.1.0(vite@7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.1.0(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) beasties: specifier: 0.3.5 version: 0.3.5 @@ -419,8 +419,8 @@ importers: specifier: 0.2.14 version: 0.2.14 vite: - specifier: 7.1.11 - version: 7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + specifier: 7.3.2 + version: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) watchpack: specifier: 2.4.4 version: 2.4.4 @@ -1671,156 +1671,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.9': resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.9': resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.9': resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.9': resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.9': resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.9': resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.9': resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.9': resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.9': resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.9': resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.9': resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.9': resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.9': resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.9': resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.9': resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.9': resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.9': resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.9': resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.9': resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.9': resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.9': resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.9': resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5117,6 +5273,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -8710,48 +8871,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@7.1.11: - resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vite@7.1.5: - resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -10199,81 +10320,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.9': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + '@esbuild/android-arm64@0.25.9': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + '@esbuild/android-arm@0.25.9': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + '@esbuild/android-x64@0.25.9': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + '@esbuild/darwin-arm64@0.25.9': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + '@esbuild/darwin-x64@0.25.9': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + '@esbuild/freebsd-arm64@0.25.9': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + '@esbuild/freebsd-x64@0.25.9': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + '@esbuild/linux-arm64@0.25.9': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + '@esbuild/linux-arm@0.25.9': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + '@esbuild/linux-ia32@0.25.9': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + '@esbuild/linux-loong64@0.25.9': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + '@esbuild/linux-mips64el@0.25.9': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + '@esbuild/linux-ppc64@0.25.9': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + '@esbuild/linux-riscv64@0.25.9': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + '@esbuild/linux-s390x@0.25.9': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + '@esbuild/linux-x64@0.25.9': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + '@esbuild/netbsd-arm64@0.25.9': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + '@esbuild/netbsd-x64@0.25.9': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + '@esbuild/openbsd-arm64@0.25.9': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + '@esbuild/openbsd-x64@0.25.9': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + '@esbuild/openharmony-arm64@0.25.9': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + '@esbuild/sunos-x64@0.25.9': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + '@esbuild/win32-arm64@0.25.9': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + '@esbuild/win32-ia32@0.25.9': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + '@esbuild/win32-x64@0.25.9': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.33.0(jiti@1.21.7))': dependencies: eslint: 9.33.0(jiti@1.21.7) @@ -12381,9 +12580,9 @@ snapshots: lodash: 4.17.21 minimatch: 7.4.6 - '@vitejs/plugin-basic-ssl@2.1.0(vite@7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - vite: 7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) '@vitest/expect@3.2.4': dependencies: @@ -12393,13 +12592,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -14095,6 +14294,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -18280,7 +18508,7 @@ snapshots: debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -18295,27 +18523,9 @@ snapshots: - tsx - yaml - vite@7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): + vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: - esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.6 - rollup: 4.59.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.9.1 - fsevents: 2.3.3 - jiti: 1.21.7 - less: 4.4.0 - sass: 1.90.0 - terser: 5.43.1 - tsx: 4.20.6 - yaml: 2.8.1 - - vite@7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - esbuild: 0.25.9 + esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.6 @@ -18335,7 +18545,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -18353,7 +18563,7 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) vite-node: 3.2.4(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: From afe50b7b1f2b2fc49f4fc316abfd3aae3c26b2eb Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Wed, 8 Apr 2026 15:23:16 -0700 Subject: [PATCH 20/26] release: cut the v20.3.23 release --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb5cec8cc85..f872741dfee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.3.23 (2026-04-08) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------- | +| [ccab02ba0](https://github.com/angular/angular-cli/commit/ccab02ba0413f25464a6e4cb5871716b221013b7) | fix | update vite to `7.3.2` | + + + # 20.3.22 (2026-03-27) diff --git a/package.json b/package.json index 00812901c745..8b8693cc8db1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.22", + "version": "20.3.23", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 10c09c77b75602293377b962b2a8397a2819036c Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Tue, 14 Apr 2026 14:15:56 -0700 Subject: [PATCH 21/26] fix(@angular/build): update esbuild to `0.28.0` This addresses some security vulnerabilities. --- package.json | 4 +- packages/angular/build/package.json | 2 +- .../angular_devkit/build_angular/package.json | 4 +- pnpm-lock.yaml | 387 +++++++++++++++--- 4 files changed, 332 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index 8b8693cc8db1..0de95a6f3cb0 100644 --- a/package.json +++ b/package.json @@ -100,8 +100,8 @@ "ansi-colors": "4.1.3", "beasties": "0.3.5", "buffer": "6.0.3", - "esbuild": "0.25.9", - "esbuild-wasm": "0.25.9", + "esbuild": "0.28.0", + "esbuild-wasm": "0.28.0", "eslint": "9.33.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-header": "3.1.1", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 50d9ae359b89..a0e52d876ad5 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -27,7 +27,7 @@ "@vitejs/plugin-basic-ssl": "2.1.0", "beasties": "0.3.5", "browserslist": "^4.23.0", - "esbuild": "0.25.9", + "esbuild": "0.28.0", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 1fc0a9d4bbdd..28c8fc10bd03 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -28,7 +28,7 @@ "browserslist": "^4.21.5", "copy-webpack-plugin": "14.0.0", "css-loader": "7.1.2", - "esbuild-wasm": "0.25.9", + "esbuild-wasm": "0.28.0", "fast-glob": "3.3.3", "http-proxy-middleware": "3.0.5", "istanbul-lib-instrument": "6.0.3", @@ -62,7 +62,7 @@ "webpack-subresource-integrity": "5.1.0" }, "optionalDependencies": { - "esbuild": "0.25.9" + "esbuild": "0.28.0" }, "devDependencies": { "@angular/ssr": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2d4169ee6e6..d4cac7ae7c1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,11 +182,11 @@ importers: specifier: 6.0.3 version: 6.0.3 esbuild: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 esbuild-wasm: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 eslint: specifier: 9.33.0 version: 9.33.0(jiti@1.21.7) @@ -374,8 +374,8 @@ importers: specifier: ^4.23.0 version: 4.26.3 esbuild: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 https-proxy-agent: specifier: 7.0.6 version: 7.0.6(supports-color@10.2.2) @@ -643,19 +643,19 @@ importers: version: 10.4.21(postcss@8.5.6) babel-loader: specifier: 10.0.0 - version: 10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.25.9)) + version: 10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.28.0)) browserslist: specifier: ^4.21.5 version: 4.26.3 copy-webpack-plugin: specifier: 14.0.0 - version: 14.0.0(webpack@5.105.0(esbuild@0.25.9)) + version: 14.0.0(webpack@5.105.0(esbuild@0.28.0)) css-loader: specifier: 7.1.2 - version: 7.1.2(webpack@5.105.0(esbuild@0.25.9)) + version: 7.1.2(webpack@5.105.0(esbuild@0.28.0)) esbuild-wasm: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 fast-glob: specifier: 3.3.3 version: 3.3.3 @@ -676,16 +676,16 @@ importers: version: 4.4.0 less-loader: specifier: 12.3.0 - version: 12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.25.9)) + version: 12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.28.0)) license-webpack-plugin: specifier: 4.0.2 - version: 4.0.2(webpack@5.105.0(esbuild@0.25.9)) + version: 4.0.2(webpack@5.105.0(esbuild@0.28.0)) loader-utils: specifier: 3.3.1 version: 3.3.1 mini-css-extract-plugin: specifier: 2.9.4 - version: 2.9.4(webpack@5.105.0(esbuild@0.25.9)) + version: 2.9.4(webpack@5.105.0(esbuild@0.28.0)) open: specifier: 10.2.0 version: 10.2.0 @@ -703,7 +703,7 @@ importers: version: 8.5.6 postcss-loader: specifier: 8.1.1 - version: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.25.9)) + version: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)) resolve-url-loader: specifier: 5.0.0 version: 5.0.0 @@ -715,13 +715,13 @@ importers: version: 1.90.0 sass-loader: specifier: 16.0.5 - version: 16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.25.9)) + version: 16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.28.0)) semver: specifier: 7.7.2 version: 7.7.2 source-map-loader: specifier: 5.0.0 - version: 5.0.0(webpack@5.105.0(esbuild@0.25.9)) + version: 5.0.0(webpack@5.105.0(esbuild@0.28.0)) source-map-support: specifier: 0.5.21 version: 0.5.21 @@ -736,19 +736,19 @@ importers: version: 2.8.1 webpack: specifier: 5.105.0 - version: 5.105.0(esbuild@0.25.9) + version: 5.105.0(esbuild@0.28.0) webpack-dev-middleware: specifier: 7.4.2 - version: 7.4.2(webpack@5.105.0(esbuild@0.25.9)) + version: 7.4.2(webpack@5.105.0(esbuild@0.28.0)) webpack-dev-server: specifier: 5.2.2 - version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.25.9)) + version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.28.0)) webpack-merge: specifier: 6.0.1 version: 6.0.1 webpack-subresource-integrity: specifier: 5.1.0 - version: 5.1.0(webpack@5.105.0(esbuild@0.25.9)) + version: 5.1.0(webpack@5.105.0(esbuild@0.28.0)) devDependencies: '@angular/ssr': specifier: workspace:* @@ -767,8 +767,8 @@ importers: version: 7.13.0 optionalDependencies: esbuild: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 packages/angular_devkit/build_webpack: dependencies: @@ -787,10 +787,10 @@ importers: version: link:../../ngtools/webpack webpack: specifier: 5.105.0 - version: 5.105.0(esbuild@0.25.9) + version: 5.105.0(esbuild@0.28.0) webpack-dev-server: specifier: 5.2.2 - version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.25.9)) + version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.28.0)) packages/angular_devkit/core: dependencies: @@ -869,7 +869,7 @@ importers: version: 5.9.2 webpack: specifier: 5.105.0 - version: 5.105.0(esbuild@0.25.9) + version: 5.105.0(esbuild@0.28.0) packages/schematics/angular: dependencies: @@ -1677,6 +1677,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.9': resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} @@ -1689,6 +1695,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.9': resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} @@ -1701,6 +1713,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.9': resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} @@ -1713,6 +1731,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.9': resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} @@ -1725,6 +1749,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.9': resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} @@ -1737,6 +1767,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.9': resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} @@ -1749,6 +1785,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} @@ -1761,6 +1803,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.9': resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} @@ -1773,6 +1821,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.9': resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} @@ -1785,6 +1839,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.9': resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} @@ -1797,6 +1857,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.9': resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} @@ -1809,6 +1875,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.9': resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} @@ -1821,6 +1893,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.9': resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} @@ -1833,6 +1911,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.9': resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} @@ -1845,6 +1929,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.9': resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} @@ -1857,6 +1947,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.9': resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} @@ -1869,6 +1965,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.9': resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} @@ -1881,6 +1983,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} @@ -1893,6 +2001,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.9': resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} @@ -1905,6 +2019,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} @@ -1917,6 +2037,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.9': resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} @@ -1929,6 +2055,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.9': resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} @@ -1941,6 +2073,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.9': resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} @@ -1953,6 +2091,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.9': resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} @@ -1965,6 +2109,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.9': resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} @@ -1977,6 +2127,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5263,8 +5419,8 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} - esbuild-wasm@0.25.9: - resolution: {integrity: sha512-Jpv5tCSwQg18aCqCRD3oHIX/prBhXMDapIoG//A+6+dV0e7KQMGFg85ihJ5T1EeMjbZjON3TqFy0VrGAnIHLDA==} + esbuild-wasm@0.28.0: + resolution: {integrity: sha512-5TRVKExcEmeMkccIZMzUq+Az6X2RoMAJyfl6SMMO1dMVhmvt0I2mx7gAb6zYi42n4d1ETcatFXazGKzA+aW7fg==} engines: {node: '>=18'} hasBin: true @@ -5278,6 +5434,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -7737,7 +7898,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qjobs@1.2.0: @@ -10323,156 +10483,234 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.0': + optional: true + '@esbuild/android-arm64@0.25.9': optional: true '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.0': + optional: true + '@esbuild/android-arm@0.25.9': optional: true '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.0': + optional: true + '@esbuild/android-x64@0.25.9': optional: true '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.0': + optional: true + '@esbuild/darwin-arm64@0.25.9': optional: true '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.0': + optional: true + '@esbuild/darwin-x64@0.25.9': optional: true '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.0': + optional: true + '@esbuild/freebsd-arm64@0.25.9': optional: true '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.0': + optional: true + '@esbuild/freebsd-x64@0.25.9': optional: true '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.0': + optional: true + '@esbuild/linux-arm64@0.25.9': optional: true '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.0': + optional: true + '@esbuild/linux-arm@0.25.9': optional: true '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.0': + optional: true + '@esbuild/linux-ia32@0.25.9': optional: true '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.0': + optional: true + '@esbuild/linux-loong64@0.25.9': optional: true '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.0': + optional: true + '@esbuild/linux-mips64el@0.25.9': optional: true '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.0': + optional: true + '@esbuild/linux-ppc64@0.25.9': optional: true '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.0': + optional: true + '@esbuild/linux-riscv64@0.25.9': optional: true '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.0': + optional: true + '@esbuild/linux-s390x@0.25.9': optional: true '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.0': + optional: true + '@esbuild/linux-x64@0.25.9': optional: true '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.0': + optional: true + '@esbuild/netbsd-arm64@0.25.9': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.0': + optional: true + '@esbuild/netbsd-x64@0.25.9': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.0': + optional: true + '@esbuild/openbsd-arm64@0.25.9': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.0': + optional: true + '@esbuild/openbsd-x64@0.25.9': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.0': + optional: true + '@esbuild/openharmony-arm64@0.25.9': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.0': + optional: true + '@esbuild/sunos-x64@0.25.9': optional: true '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.0': + optional: true + '@esbuild/win32-arm64@0.25.9': optional: true '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.0': + optional: true + '@esbuild/win32-ia32@0.25.9': optional: true '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.0': + optional: true + '@esbuild/win32-x64@0.25.9': optional: true '@esbuild/win32-x64@0.27.7': optional: true + '@esbuild/win32-x64@0.28.0': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.33.0(jiti@1.21.7))': dependencies: eslint: 9.33.0(jiti@1.21.7) @@ -13150,11 +13388,11 @@ snapshots: b4a@1.7.3: {} - babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.25.9)): + babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.28.0)): dependencies: '@babel/core': 7.28.3 find-up: 5.0.0 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): dependencies: @@ -13757,14 +13995,14 @@ snapshots: dependencies: is-what: 3.14.1 - copy-webpack-plugin@14.0.0(webpack@5.105.0(esbuild@0.25.9)): + copy-webpack-plugin@14.0.0(webpack@5.105.0(esbuild@0.28.0)): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 7.0.3 tinyglobby: 0.2.14 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) core-js-compat@3.46.0: dependencies: @@ -13808,7 +14046,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@7.1.2(webpack@5.105.0(esbuild@0.25.9)): + css-loader@7.1.2(webpack@5.105.0(esbuild@0.28.0)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -13819,7 +14057,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) css-select@6.0.0: dependencies: @@ -14263,7 +14501,7 @@ snapshots: dependencies: es6-promise: 4.2.8 - esbuild-wasm@0.25.9: {} + esbuild-wasm@0.28.0: {} esbuild@0.25.9: optionalDependencies: @@ -14323,6 +14561,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -15979,11 +16246,11 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.3 - less-loader@12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.25.9)): + less-loader@12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.28.0)): dependencies: less: 4.4.0 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) less@4.4.0: dependencies: @@ -16004,11 +16271,11 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.105.0(esbuild@0.25.9)): + license-webpack-plugin@4.0.2(webpack@5.105.0(esbuild@0.28.0)): dependencies: webpack-sources: 3.3.3 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) lie@3.3.0: dependencies: @@ -16241,11 +16508,11 @@ snapshots: mimic-function@5.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.105.0(esbuild@0.25.9)): + mini-css-extract-plugin@2.9.4(webpack@5.105.0(esbuild@0.28.0)): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) minimalistic-assert@1.0.1: {} @@ -16894,14 +17161,14 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.25.9)): + postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)): dependencies: cosmiconfig: 9.0.0(typescript@5.9.2) jiti: 1.21.7 postcss: 8.5.6 semver: 7.7.2 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) transitivePeerDependencies: - typescript @@ -17442,12 +17709,12 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.25.9)): + sass-loader@16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.28.0)): dependencies: neo-async: 2.6.2 optionalDependencies: sass: 1.90.0 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) sass@1.90.0: dependencies: @@ -17770,11 +18037,11 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.105.0(esbuild@0.25.9)): + source-map-loader@5.0.0(webpack@5.105.0(esbuild@0.28.0)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) source-map-support@0.4.18: dependencies: @@ -18073,16 +18340,16 @@ snapshots: transitivePeerDependencies: - supports-color - terser-webpack-plugin@5.3.16(esbuild@0.25.9)(webpack@5.105.0(esbuild@0.25.9)): + terser-webpack-plugin@5.3.16(esbuild@0.28.0)(webpack@5.105.0(esbuild@0.28.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) optionalDependencies: - esbuild: 0.25.9 + esbuild: 0.28.0 terser@5.43.1: dependencies: @@ -18635,7 +18902,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-dev-middleware@7.4.2(webpack@5.105.0(esbuild@0.25.9)): + webpack-dev-middleware@7.4.2(webpack@5.105.0(esbuild@0.28.0)): dependencies: colorette: 2.0.20 memfs: 4.49.0 @@ -18644,9 +18911,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) - webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.25.9)): + webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.28.0)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -18674,10 +18941,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.105.0(esbuild@0.25.9)) + webpack-dev-middleware: 7.4.2(webpack@5.105.0(esbuild@0.28.0)) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) transitivePeerDependencies: - bufferutil - debug @@ -18692,12 +18959,12 @@ snapshots: webpack-sources@3.3.3: {} - webpack-subresource-integrity@5.1.0(webpack@5.105.0(esbuild@0.25.9)): + webpack-subresource-integrity@5.1.0(webpack@5.105.0(esbuild@0.28.0)): dependencies: typed-assert: 1.0.9 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) - webpack@5.105.0(esbuild@0.25.9): + webpack@5.105.0(esbuild@0.28.0): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -18721,7 +18988,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(esbuild@0.25.9)(webpack@5.105.0(esbuild@0.25.9)) + terser-webpack-plugin: 5.3.16(esbuild@0.28.0)(webpack@5.105.0(esbuild@0.28.0)) watchpack: 2.5.1 webpack-sources: 3.3.3 transitivePeerDependencies: From 54112572992d7e940981f5487d50f76b82db7988 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:10:18 -0400 Subject: [PATCH 22/26] release: cut the v20.3.24 release --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f872741dfee4..4c093b0c4a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + + +# 20.3.24 (2026-04-15) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------- | +| [10c09c77b](https://github.com/angular/angular-cli/commit/10c09c77b75602293377b962b2a8397a2819036c) | fix | update esbuild to `0.28.0` | + + + # 20.3.23 (2026-04-08) diff --git a/package.json b/package.json index 0de95a6f3cb0..c12f9964b448 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.23", + "version": "20.3.24", "private": true, "description": "Software Development Kit for Angular", "keywords": [ From 6686848d946ca157f9b92b84db377e912266395e Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 24 Apr 2026 10:07:37 +0200 Subject: [PATCH 23/26] fix(@angular/ssr): introduce trustProxyHeaders option to safely validate and sanitize proxy headers This commit adds the `trustProxyHeaders` option to `AngularAppEngineOptions` and `AngularNodeAppEngineOptions` to configure, validate, and sanitize `X-Forwarded-*` headers. - When `trustProxyHeaders` is `undefined` (default): - Allows `X-Forwarded-Host` and `X-Forwarded-Proto`. - Intercepts `X-Forwarded-Prefix` and triggers a dynamic CSR deoptimization to skip SSR if present. - Logs an informative message when receiving any other `X-Forwarded-*` headers. - When `false`: - Ignores and strips all proxy headers from the request. - When `true`: - Trusts all proxy headers. - When a string array: - Allows only the proxy headers provided inside the array. --- goldens/public-api/angular/ssr/index.api.md | 2 + .../public-api/angular/ssr/node/index.api.md | 2 +- packages/angular/ssr/node/src/app-engine.ts | 6 +- packages/angular/ssr/node/src/request.ts | 65 ++++- packages/angular/ssr/node/test/BUILD.bazel | 1 + .../angular/ssr/node/test/request_spec.ts | 24 +- packages/angular/ssr/src/app-engine.ts | 73 +++++- packages/angular/ssr/src/app.ts | 3 +- packages/angular/ssr/src/utils/validation.ts | 225 +++++++++--------- packages/angular/ssr/test/app-engine_spec.ts | 29 ++- .../angular/ssr/test/utils/validation_spec.ts | 200 +++++++--------- 11 files changed, 359 insertions(+), 271 deletions(-) diff --git a/goldens/public-api/angular/ssr/index.api.md b/goldens/public-api/angular/ssr/index.api.md index cbdacabfd7f6..1c2f8f2c625f 100644 --- a/goldens/public-api/angular/ssr/index.api.md +++ b/goldens/public-api/angular/ssr/index.api.md @@ -14,12 +14,14 @@ export class AngularAppEngine { constructor(options?: AngularAppEngineOptions); handle(request: Request, requestContext?: unknown): Promise; static ɵallowStaticRouteRender: boolean; + static ɵdisableAllowedHostsCheck: boolean; static ɵhooks: Hooks; } // @public export interface AngularAppEngineOptions { allowedHosts?: readonly string[]; + trustProxyHeaders?: boolean | readonly string[]; } // @public diff --git a/goldens/public-api/angular/ssr/node/index.api.md b/goldens/public-api/angular/ssr/node/index.api.md index 2c52c06b47c1..2c1c5f4bf86a 100644 --- a/goldens/public-api/angular/ssr/node/index.api.md +++ b/goldens/public-api/angular/ssr/node/index.api.md @@ -55,7 +55,7 @@ export interface CommonEngineRenderOptions { export function createNodeRequestHandler(handler: T): T; // @public -export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage | Http2ServerRequest): Request; +export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage | Http2ServerRequest, trustProxyHeaders?: boolean | readonly string[]): Request; // @public export function isMainModule(url: string): boolean; diff --git a/packages/angular/ssr/node/src/app-engine.ts b/packages/angular/ssr/node/src/app-engine.ts index 29eb1fd366ab..910a6d884e98 100644 --- a/packages/angular/ssr/node/src/app-engine.ts +++ b/packages/angular/ssr/node/src/app-engine.ts @@ -29,6 +29,7 @@ export interface AngularNodeAppEngineOptions extends AngularAppEngineOptions {} */ export class AngularNodeAppEngine { private readonly angularAppEngine: AngularAppEngine; + private readonly trustProxyHeaders?: boolean | readonly string[]; /** * Creates a new instance of the Angular Node.js server application engine. @@ -39,6 +40,7 @@ export class AngularNodeAppEngine { ...options, allowedHosts: [...getAllowedHostsFromEnv(), ...(options?.allowedHosts ?? [])], }); + this.trustProxyHeaders = options?.trustProxyHeaders; attachNodeGlobalErrorHandlers(); } @@ -76,7 +78,9 @@ export class AngularNodeAppEngine { requestContext?: unknown, ): Promise { const webRequest = - request instanceof Request ? request : createWebRequestFromNodeRequest(request); + request instanceof Request + ? request + : createWebRequestFromNodeRequest(request, this.trustProxyHeaders); return this.angularAppEngine.handle(webRequest, requestContext); } diff --git a/packages/angular/ssr/node/src/request.ts b/packages/angular/ssr/node/src/request.ts index 402ec29ba56d..dd49a3d8b24a 100644 --- a/packages/angular/ssr/node/src/request.ts +++ b/packages/angular/ssr/node/src/request.ts @@ -8,7 +8,11 @@ import type { IncomingHttpHeaders, IncomingMessage } from 'node:http'; import type { Http2ServerRequest } from 'node:http2'; -import { getFirstHeaderValue } from '../../src/utils/validation'; +import { + getFirstHeaderValue, + isProxyHeaderAllowed, + normalizeTrustProxyHeaders, +} from '../../src/utils/validation'; /** * A set containing all the pseudo-headers defined in the HTTP/2 specification. @@ -17,7 +21,13 @@ import { getFirstHeaderValue } from '../../src/utils/validation'; * as they are not allowed to be set directly using the `Node.js` Undici API or * the web `Headers` API. */ -const HTTP2_PSEUDO_HEADERS = new Set([':method', ':scheme', ':authority', ':path', ':status']); +const HTTP2_PSEUDO_HEADERS: ReadonlySet = new Set([ + ':method', + ':scheme', + ':authority', + ':path', + ':status', +]); /** * Converts a Node.js `IncomingMessage` or `Http2ServerRequest` into a @@ -27,16 +37,25 @@ const HTTP2_PSEUDO_HEADERS = new Set([':method', ':scheme', ':authority', ':path * be used by web platform APIs. * * @param nodeRequest - The Node.js request object (`IncomingMessage` or `Http2ServerRequest`) to convert. + * @param trustProxyHeaders - A boolean or an array of proxy headers to trust when constructing the request URL. + * + * @remarks + * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and + * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure + * level (e.g., at the reverse proxy or API gateway) before reaching the application. + * * @returns A Web Standard `Request` object. */ export function createWebRequestFromNodeRequest( nodeRequest: IncomingMessage | Http2ServerRequest, + trustProxyHeaders?: boolean | readonly string[], ): Request { + const trustProxyHeadersNormalized = normalizeTrustProxyHeaders(trustProxyHeaders); const { headers, method = 'GET' } = nodeRequest; const withBody = method !== 'GET' && method !== 'HEAD'; const referrer = headers.referer && URL.canParse(headers.referer) ? headers.referer : undefined; - return new Request(createRequestUrl(nodeRequest), { + return new Request(createRequestUrl(nodeRequest, trustProxyHeadersNormalized), { method, headers: createRequestHeaders(headers), body: withBody ? nodeRequest : undefined, @@ -75,20 +94,34 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers { * Creates a `URL` object from a Node.js `IncomingMessage`, taking into account the protocol, host, and port. * * @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from. + * @param trustProxyHeaders - A set of allowed proxy headers. + * + * @remarks + * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and + * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure + * level (e.g., at the reverse proxy or API gateway) before reaching the application. + * * @returns A `URL` object representing the request URL. */ -export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL { +export function createRequestUrl( + nodeRequest: IncomingMessage | Http2ServerRequest, + trustProxyHeaders: ReadonlySet, +): URL { const { headers, socket, url = '', originalUrl, } = nodeRequest as IncomingMessage & { originalUrl?: string }; + const protocol = - getFirstHeaderValue(headers['x-forwarded-proto']) ?? + getAllowedProxyHeaderValue(headers, 'x-forwarded-proto', trustProxyHeaders) ?? ('encrypted' in socket && socket.encrypted ? 'https' : 'http'); + const hostname = - getFirstHeaderValue(headers['x-forwarded-host']) ?? headers.host ?? headers[':authority']; + getAllowedProxyHeaderValue(headers, 'x-forwarded-host', trustProxyHeaders) ?? + headers.host ?? + headers[':authority']; if (Array.isArray(hostname)) { throw new Error('host value cannot be an array.'); @@ -96,7 +129,7 @@ export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerReque let hostnameWithPort = hostname; if (!hostname?.includes(':')) { - const port = getFirstHeaderValue(headers['x-forwarded-port']); + const port = getAllowedProxyHeaderValue(headers, 'x-forwarded-port', trustProxyHeaders); if (port) { hostnameWithPort += `:${port}`; } @@ -104,3 +137,21 @@ export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerReque return new URL(`${protocol}://${hostnameWithPort}${originalUrl ?? url}`); } + +/** + * Gets the first value of an allowed proxy header. + * + * @param headers - The Node.js incoming HTTP headers. + * @param headerName - The name of the proxy header to retrieve. + * @param trustProxyHeaders - A set of allowed proxy headers. + * @returns The value of the allowed proxy header, or `undefined` if not allowed or not present. + */ +function getAllowedProxyHeaderValue( + headers: IncomingHttpHeaders, + headerName: string, + trustProxyHeaders: ReadonlySet, +): string | undefined { + return isProxyHeaderAllowed(headerName, trustProxyHeaders) + ? getFirstHeaderValue(headers[headerName]) + : undefined; +} diff --git a/packages/angular/ssr/node/test/BUILD.bazel b/packages/angular/ssr/node/test/BUILD.bazel index 5a7c45126966..9610a17557b4 100644 --- a/packages/angular/ssr/node/test/BUILD.bazel +++ b/packages/angular/ssr/node/test/BUILD.bazel @@ -7,6 +7,7 @@ ts_project( srcs = glob(["**/*_spec.ts"]), deps = [ "//:node_modules/@types/node", + "//packages/angular/ssr", "//packages/angular/ssr/node", ], ) diff --git a/packages/angular/ssr/node/test/request_spec.ts b/packages/angular/ssr/node/test/request_spec.ts index 952faefd9f28..982ae898624b 100644 --- a/packages/angular/ssr/node/test/request_spec.ts +++ b/packages/angular/ssr/node/test/request_spec.ts @@ -7,8 +7,8 @@ */ import { IncomingMessage } from 'node:http'; -import { Http2ServerRequest } from 'node:http2'; import { Socket } from 'node:net'; +import { normalizeTrustProxyHeaders } from '../../src/utils/validation'; import { createRequestUrl } from '../src/request'; // Helper to create a mock request object for testing. @@ -26,18 +26,6 @@ function createRequest(details: { } as unknown as IncomingMessage; } -// Helper to create a mock Http2ServerRequest object for testing. -function createHttp2Request(details: { - headers: Record; - url?: string; -}): Http2ServerRequest { - return { - headers: details.headers, - socket: new Socket(), - url: details.url, - } as Http2ServerRequest; -} - describe('createRequestUrl', () => { it('should create a http URL with hostname and port from the host header', () => { const url = createRequestUrl( @@ -45,6 +33,7 @@ describe('createRequestUrl', () => { headers: { host: 'localhost:8080' }, url: '/test', }), + new Set(), ); expect(url.href).toBe('http://localhost:8080/test'); }); @@ -56,6 +45,7 @@ describe('createRequestUrl', () => { encryptedSocket: true, url: '/test', }), + new Set(), ); expect(url.href).toBe('https://example.com/test'); }); @@ -67,6 +57,7 @@ describe('createRequestUrl', () => { encryptedSocket: true, url: '', }), + new Set(), ); expect(url.href).toBe('https://example.com/'); }); @@ -78,6 +69,7 @@ describe('createRequestUrl', () => { encryptedSocket: true, url: '/test?a=1', }), + new Set(), ); expect(url.href).toBe('https://example.com/test?a=1'); }); @@ -90,6 +82,7 @@ describe('createRequestUrl', () => { url: '/test', originalUrl: '/original', }), + new Set(), ); expect(url.href).toBe('https://example.com/original'); }); @@ -102,6 +95,7 @@ describe('createRequestUrl', () => { url: undefined, originalUrl: undefined, }), + new Set(), ); expect(url.href).toBe('https://example.com/'); }); @@ -112,6 +106,7 @@ describe('createRequestUrl', () => { headers: { host: 'localhost:8080' }, url: '//example.com/test', }), + new Set(), ); expect(url.href).toBe('http://localhost:8080//example.com/test'); }); @@ -123,6 +118,7 @@ describe('createRequestUrl', () => { url: '/test', originalUrl: '//example.com/original', }), + new Set(), ); expect(url.href).toBe('http://localhost:8080//example.com/original'); }); @@ -137,6 +133,7 @@ describe('createRequestUrl', () => { }, url: '/test', }), + normalizeTrustProxyHeaders(true), ); expect(url.href).toBe('https://example.com/test'); }); @@ -152,6 +149,7 @@ describe('createRequestUrl', () => { }, url: '/test', }), + normalizeTrustProxyHeaders(true), ); expect(url.href).toBe('https://example.com:8443/test'); }); diff --git a/packages/angular/ssr/src/app-engine.ts b/packages/angular/ssr/src/app-engine.ts index 6242750b4c47..d65977a1a927 100644 --- a/packages/angular/ssr/src/app-engine.ts +++ b/packages/angular/ssr/src/app-engine.ts @@ -11,7 +11,11 @@ import { Hooks } from './hooks'; import { getPotentialLocaleIdFromUrl, getPreferredLocale } from './i18n'; import { EntryPointExports, getAngularAppEngineManifest } from './manifest'; import { joinUrlParts } from './utils/url'; -import { cloneRequestAndPatchHeaders, validateRequest } from './utils/validation'; +import { + normalizeTrustProxyHeaders, + sanitizeRequestHeaders, + validateRequest, +} from './utils/validation'; /** * Options for the Angular server application engine. @@ -21,6 +25,22 @@ export interface AngularAppEngineOptions { * A set of allowed hostnames for the server application. */ allowedHosts?: readonly string[]; + + /** + * Extends the scope of trusted proxy headers (`X-Forwarded-*`). + * + * @remarks + * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and + * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure + * level (e.g., at the reverse proxy or API gateway) before reaching the application. + * + * If a `string[]` is provided, only those proxy headers are allowed. + * If `true`, all proxy headers are allowed. + * If `false`, proxy headers are ignored. + * + * @default undefined + */ + trustProxyHeaders?: boolean | readonly string[]; } /** @@ -51,6 +71,13 @@ export class AngularAppEngine { */ static ɵhooks = /* #__PURE__*/ new Hooks(); + /** + * A flag to disable the allowed hosts check. + * + * @private + */ + static ɵdisableAllowedHostsCheck = false; + /** * The manifest for the server application. */ @@ -68,6 +95,11 @@ export class AngularAppEngine { this.manifest.supportedLocales, ); + /** + * The normalized allowed proxy headers. + */ + private readonly trustProxyHeaders: ReadonlySet; + /** * A cache that holds entry points, keyed by their potential locale string. */ @@ -78,7 +110,23 @@ 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); + this.trustProxyHeaders = normalizeTrustProxyHeaders(options?.trustProxyHeaders); + } + + 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, such as a load balancer or reverse proxy. ' + + 'For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf', + ); + } + + return allowedHosts; } /** @@ -106,23 +154,24 @@ export class AngularAppEngine { */ async handle(request: Request, requestContext?: unknown): Promise { const allowedHost = this.allowedHosts; + const { request: securedRequest, deoptToCSR } = sanitizeRequestHeaders( + request, + this.trustProxyHeaders, + ); try { - validateRequest(request, allowedHost); + validateRequest(securedRequest, allowedHost, AngularAppEngine.ɵdisableAllowedHostsCheck); } catch (error) { - return this.handleValidationError(error as Error, request); + return this.handleValidationError(error as Error, securedRequest); } - // Clone request with patched headers to prevent unallowed host header access. - const { request: securedRequest, onError: onHeaderValidationError } = - cloneRequestAndPatchHeaders(request, allowedHost); - const serverApp = await this.getAngularServerAppForRequest(securedRequest); if (serverApp) { - return Promise.race([ - onHeaderValidationError.then((error) => this.handleValidationError(error, securedRequest)), - serverApp.handle(securedRequest, requestContext), - ]); + if (deoptToCSR) { + return serverApp.serveClientSidePage(); + } + + return serverApp.handle(securedRequest, requestContext); } if (this.supportedLocales.length > 1) { diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index d3e3b1dfa7bf..c2aea694bb71 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -175,8 +175,7 @@ export class AngularServerApp { return null; } - const { redirectTo, status, renderMode, headers } = matchedRoute; - + const { redirectTo, status, renderMode, headers, preload } = matchedRoute; if (redirectTo !== undefined) { return createRedirectResponse( joinUrlParts( diff --git a/packages/angular/ssr/src/utils/validation.ts b/packages/angular/ssr/src/utils/validation.ts index 9e83e144b347..d27a52f51b9f 100644 --- a/packages/angular/ssr/src/utils/validation.ts +++ b/packages/angular/ssr/src/utils/validation.ts @@ -6,10 +6,21 @@ * found in the LICENSE file at https://angular.dev/license */ +/** + * Common X-Forwarded-* headers. + */ +const X_FORWARDED_HEADERS = new Set([ + 'x-forwarded-for', + 'x-forwarded-host', + 'x-forwarded-port', + 'x-forwarded-proto', + 'x-forwarded-prefix', +]); + /** * The set of headers that should be validated for host header injection attacks. */ -const HOST_HEADERS_TO_VALIDATE: ReadonlySet = new Set(['host', 'x-forwarded-host']); +const HOST_HEADERS_TO_VALIDATE: ReadonlyArray = ['host', 'x-forwarded-host']; /** * Regular expression to validate that the port is a numeric value. @@ -21,15 +32,10 @@ const VALID_PORT_REGEX = /^\d+$/; */ const VALID_PROTO_REGEX = /^https?$/i; -/** - * Regular expression to validate that the host is a valid hostname. - */ -const VALID_HOST_REGEX = /^[a-z0-9.:-]+$/i; - /** * Regular expression to validate that the prefix is valid. */ -const INVALID_PREFIX_REGEX = /^(?:\\|\/[/\\])|(?:^|[/\\])\.\.?(?:[/\\]|$)/; +const VALID_PREFIX_REGEX = /^\/([a-z0-9_-]+\/)*[a-z0-9_-]*$/i; /** * Extracts the first value from a multi-value header string. @@ -58,9 +64,16 @@ export function getFirstHeaderValue( * @param allowedHosts - A set of allowed hostnames. * @throws Error if any of the validated headers contain invalid values. */ -export function validateRequest(request: Request, allowedHosts: ReadonlySet): void { - validateHeaders(request); - validateUrl(new URL(request.url), allowedHosts); +export function validateRequest( + request: Request, + allowedHosts: ReadonlySet, + disableHostCheck: boolean, +): void { + validateHeaders(request, allowedHosts, disableHostCheck); + + if (!disableHostCheck) { + validateUrl(new URL(request.url), allowedHosts); + } } /** @@ -78,106 +91,47 @@ export function validateUrl(url: URL, allowedHosts: ReadonlySet): void { } /** - * Clones a request and patches the `get` method of the request headers to validate the host headers. - * @param request - The request to validate. - * @param allowedHosts - A set of allowed hostnames. - * @returns An object containing the cloned request and a promise that resolves to an error - * if any of the validated headers contain invalid values. + * Sanitizes the proxy headers of a request by removing unallowed `X-Forwarded-*` headers. + * If no headers need to be removed, the original request is returned without cloning. + * + * @param request - The incoming `Request` object to sanitize. + * @param trustProxyHeaders - A set of allowed proxy headers. + * @returns An object containing the sanitized request, or the original request if no changes were needed. */ -export function cloneRequestAndPatchHeaders( +export function sanitizeRequestHeaders( request: Request, - allowedHosts: ReadonlySet, -): { request: Request; onError: Promise } { - let onError: (value: Error) => void; - const onErrorPromise = new Promise((resolve) => { - onError = resolve; - }); + trustProxyHeaders: ReadonlySet, +): { request: Request; deoptToCSR: boolean } { + const keysToDelete: string[] = []; + let deoptToCSR = false; + + for (const [key] of request.headers) { + const lowerKey = key.toLowerCase(); + if (lowerKey.startsWith('x-forwarded-') && !isProxyHeaderAllowed(lowerKey, trustProxyHeaders)) { + // eslint-disable-next-line no-console + console.warn( + `Received "${key}" header but "trustProxyHeaders" was not set up to allow it.\n` + + `For more information, see https://angular.dev/best-practices/security#configuring-trusted-proxy-headers`, + ); + deoptToCSR = true; + keysToDelete.push(key); + } + } + + if (keysToDelete.length === 0) { + return { request, deoptToCSR }; + } const clonedReq = new Request(request.clone(), { signal: request.signal, }); const headers = clonedReq.headers; - - const originalGet = headers.get; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - (headers.get as typeof originalGet) = function (name) { - const value = originalGet.call(headers, name); - if (!value) { - return value; - } - - validateHeader(name, value, allowedHosts, onError); - - return value; - }; - - const originalValues = headers.values; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - (headers.values as typeof originalValues) = function () { - for (const name of HOST_HEADERS_TO_VALIDATE) { - validateHeader(name, originalGet.call(headers, name), allowedHosts, onError); - } - - return originalValues.call(headers); - }; - - const originalEntries = headers.entries; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - (headers.entries as typeof originalEntries) = function () { - const iterator = originalEntries.call(headers); - - return { - next() { - const result = iterator.next(); - if (!result.done) { - const [key, value] = result.value; - validateHeader(key, value, allowedHosts, onError); - } - - return result; - }, - [Symbol.iterator]() { - return this; - }, - }; - }; - - // Ensure for...of loops use the new patched entries - (headers[Symbol.iterator] as typeof originalEntries) = headers.entries; - - return { request: clonedReq, onError: onErrorPromise }; -} - -/** - * Validates a specific header value against the allowed hosts. - * @param name - The name of the header to validate. - * @param value - The value of the header to validate. - * @param allowedHosts - A set of allowed hostnames. - * @param onError - A callback function to call if the header value is invalid. - * @throws Error if the header value is invalid. - */ -function validateHeader( - name: string, - value: string | null, - allowedHosts: ReadonlySet, - onError: (value: Error) => void, -): void { - if (!value) { - return; + for (const key of keysToDelete) { + headers.delete(key); } - if (!HOST_HEADERS_TO_VALIDATE.has(name.toLowerCase())) { - return; - } - - try { - verifyHostAllowed(name, value, allowedHosts); - } catch (error) { - onError(error as Error); - - throw error; - } + return { request: clonedReq, deoptToCSR }; } /** @@ -193,19 +147,20 @@ function verifyHostAllowed( headerValue: string, allowedHosts: ReadonlySet, ): void { - const value = getFirstHeaderValue(headerValue); - if (!value) { - return; - } - - const url = `http://${value}`; + const url = `http://${headerValue}`; if (!URL.canParse(url)) { throw new Error(`Header "${headerName}" contains an invalid value and cannot be parsed.`); } - const { hostname } = new URL(url); + const { hostname, pathname, search, hash, username, password } = new URL(url); + if (pathname !== '/' || search || hash || username || password) { + throw new Error( + `Header "${headerName}" with value "${headerValue}" contains characters that are not allowed.`, + ); + } + if (!isHostAllowed(hostname, allowedHosts)) { - throw new Error(`Header "${headerName}" with value "${value}" is not allowed.`); + throw new Error(`Header "${headerName}" with value "${headerValue}" is not allowed.`); } } @@ -238,14 +193,20 @@ function isHostAllowed(hostname: string, allowedHosts: ReadonlySet): boo * Validates the headers of an incoming request. * * @param request - The incoming `Request` object containing the headers to validate. + * @param allowedHosts - A set of allowed hostnames. + * @param disableHostCheck - Whether to disable the host check. * @throws Error if any of the validated headers contain invalid values. */ -function validateHeaders(request: Request): void { +function validateHeaders( + request: Request, + allowedHosts: ReadonlySet, + disableHostCheck: boolean, +): void { const headers = request.headers; for (const headerName of HOST_HEADERS_TO_VALIDATE) { const headerValue = getFirstHeaderValue(headers.get(headerName)); - if (headerValue && !VALID_HOST_REGEX.test(headerValue)) { - throw new Error(`Header "${headerName}" contains characters that are not allowed.`); + if (headerValue && !disableHostCheck) { + verifyHostAllowed(headerName, headerValue, allowedHosts); } } @@ -260,9 +221,47 @@ function validateHeaders(request: Request): void { } const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix')); - if (xForwardedPrefix && INVALID_PREFIX_REGEX.test(xForwardedPrefix)) { + if (xForwardedPrefix && !VALID_PREFIX_REGEX.test(xForwardedPrefix)) { throw new Error( - 'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.', + 'Header "x-forwarded-prefix" is invalid. It must start with a "/" and contain ' + + 'only alphanumeric characters, hyphens, and underscores, separated by single slashes.', ); } } + +/** + * Checks if a specific proxy header is allowed. + * + * @param headerName - The name of the proxy header to check. + * @param trustProxyHeaders - A set of allowed proxy headers. + * @returns `true` if the header is allowed, `false` otherwise. + */ +export function isProxyHeaderAllowed( + headerName: string, + trustProxyHeaders: ReadonlySet, +): boolean { + return trustProxyHeaders.has(headerName.toLowerCase()); +} + +/** + * Normalizes the `trustProxyHeaders` option to a consistent representation. + * @param trustProxyHeaders The input `trustProxyHeaders` value. + * @returns A `Set` of normalized header names. + */ +export function normalizeTrustProxyHeaders( + trustProxyHeaders: boolean | readonly string[] | undefined, +): ReadonlySet { + if (trustProxyHeaders === undefined) { + return new Set(['x-forwarded-host', 'x-forwarded-proto']); + } + + if (trustProxyHeaders === false) { + return new Set(); + } + + if (trustProxyHeaders === true) { + return X_FORWARDED_HEADERS; + } + + return new Set(trustProxyHeaders.map((h) => h.toLowerCase())); +} diff --git a/packages/angular/ssr/test/app-engine_spec.ts b/packages/angular/ssr/test/app-engine_spec.ts index 738f7c695cc3..2f8359a8b0d8 100644 --- a/packages/angular/ssr/test/app-engine_spec.ts +++ b/packages/angular/ssr/test/app-engine_spec.ts @@ -108,7 +108,7 @@ describe('AngularAppEngine', () => { basePath: '/', }); - appEngine = new AngularAppEngine(); + appEngine = new AngularAppEngine({ trustProxyHeaders: true }); }); describe('handle', () => { @@ -166,6 +166,21 @@ describe('AngularAppEngine', () => { expect(response?.headers.get('Vary')).toBe('Accept-Language'); }); + it('should completely ignore proxy headers if not allowed', async () => { + const strictEngine = new AngularAppEngine({ trustProxyHeaders: false }); + const request = new Request('https://example.com', { + headers: { + 'Accept-Language': 'it', + 'X-Forwarded-Prefix': '/app', + }, + }); + + // The strictEngine will ignore the prefix + const response = await strictEngine.handle(request); + expect(response?.status).toBe(302); + expect(response?.headers.get('Location')).toBe('/it'); + }); + it('should return null for requests to file-like resources in a locale', async () => { const request = new Request('https://example.com/it/logo.png'); const response = await appEngine.handle(request); @@ -315,7 +330,7 @@ describe('AngularAppEngine', () => { supportedLocales: { 'en-US': '' }, }); - appEngine = new AngularAppEngine(); + appEngine = new AngularAppEngine({ trustProxyHeaders: true }); }); beforeEach(() => { @@ -371,10 +386,12 @@ describe('AngularAppEngine', () => { expect(response).not.toBeNull(); expect(response?.status).toBe(400); expect(await response?.text()).toContain( - 'Header "host" contains characters that are not allowed.', + 'Header "host" with value "example.com/evil" contains characters that are not allowed.', ); expect(consoleErrorSpy).toHaveBeenCalledWith( - jasmine.stringMatching('Header "host" contains characters that are not allowed.'), + jasmine.stringMatching( + 'Header "host" with value "example.com/evil" contains characters that are not allowed.', + ), ); }); }); @@ -425,7 +442,9 @@ describe('AngularAppEngine', () => { expect(response).not.toBeNull(); expect(await response?.text()).toContain('CSR page'); expect(consoleErrorSpy).toHaveBeenCalledWith( - jasmine.stringMatching('Header "host" contains characters that are not allowed.'), + jasmine.stringMatching( + 'Header "host" with value "example.com/evil" contains characters that are not allowed.', + ), ); }); }); diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts index acf1e4829e8e..ec50fb07b502 100644 --- a/packages/angular/ssr/test/utils/validation_spec.ts +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -7,8 +7,9 @@ */ import { - cloneRequestAndPatchHeaders, getFirstHeaderValue, + normalizeTrustProxyHeaders, + sanitizeRequestHeaders, validateRequest, validateUrl, } from '../../src/utils/validation'; @@ -77,13 +78,13 @@ describe('Validation Utils', () => { }, }); - expect(() => validateRequest(req, allowedHosts)).not.toThrow(); + expect(() => validateRequest(req, allowedHosts, false)).not.toThrow(); }); it('should throw if URL hostname is invalid', () => { const req = new Request('http://evil.com'); - expect(() => validateRequest(req, allowedHosts)).toThrowError( + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( /URL with hostname "evil.com" is not allowed/, ); }); @@ -93,7 +94,7 @@ describe('Validation Utils', () => { headers: { 'x-forwarded-port': 'abc' }, }); - expect(() => validateRequest(req, allowedHosts)).toThrowError( + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( 'Header "x-forwarded-port" must be a numeric value.', ); }); @@ -102,7 +103,7 @@ describe('Validation Utils', () => { const req = new Request('http://example.com', { headers: { 'x-forwarded-proto': 'ftp' }, }); - expect(() => validateRequest(req, allowedHosts)).toThrowError( + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( 'Header "x-forwarded-proto" must be either "http" or "https".', ); }); @@ -111,40 +112,36 @@ describe('Validation Utils', () => { const req = new Request('http://example.com', { headers: { 'host': 'example.com/bad' }, }); - expect(() => validateRequest(req, allowedHosts)).toThrowError( - 'Header "host" contains characters that are not allowed.', + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "host" with value "example.com/bad" contains characters that are not allowed.', ); }); - it('should throw if x-forwarded-host contains path separators', () => { + it('should throw if host contains invalid characters', () => { const req = new Request('http://example.com', { - headers: { 'x-forwarded-host': 'example.com/bad' }, + headers: { 'host': 'example.com?query=1' }, }); - expect(() => validateRequest(req, allowedHosts)).toThrowError( - 'Header "x-forwarded-host" contains characters that are not allowed.', + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "host" with value "example.com?query=1" contains characters that are not allowed.', ); }); - it('should throw error if x-forwarded-prefix starts with a backslash or multiple slashes', () => { - const inputs = ['//evil', '\\\\evil', '/\\evil', '\\/evil', '\\evil']; - - for (const prefix of inputs) { - const request = new Request('https://example.com', { - headers: { - 'x-forwarded-prefix': prefix, - }, - }); - - expect(() => validateRequest(request, allowedHosts)) - .withContext(`Prefix: "${prefix}"`) - .toThrowError( - 'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.', - ); - } + it('should throw if x-forwarded-host contains path separators', () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-host': 'example.com/bad' }, + }); + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "x-forwarded-host" with value "example.com/bad" contains characters that are not allowed.', + ); }); - it('should throw error if x-forwarded-prefix contains dot segments', () => { + it('should throw error if x-forwarded-prefix is invalid', () => { const inputs = [ + '//evil', + '\\\\evil', + '/\\evil', + '\\/evil', + '\\evil', '/./', '/../', '/foo/./bar', @@ -159,6 +156,11 @@ describe('Validation Utils', () => { '/foo/..\\bar', '.', '..', + 'https://attacker.com', + '%2f%2f', + '%5C', + 'http://localhost', + 'foo', ]; for (const prefix of inputs) { @@ -168,16 +170,17 @@ describe('Validation Utils', () => { }, }); - expect(() => validateRequest(request, allowedHosts)) + expect(() => validateRequest(request, allowedHosts, false)) .withContext(`Prefix: "${prefix}"`) .toThrowError( - 'Header "x-forwarded-prefix" must not start with "\\" or multiple "/" or contain ".", ".." path segments.', + 'Header "x-forwarded-prefix" is invalid. It must start with a "/" and contain ' + + 'only alphanumeric characters, hyphens, and underscores, separated by single slashes.', ); } }); - it('should validate x-forwarded-prefix with valid dot usage', () => { - const inputs = ['/foo.bar', '/foo.bar/baz', '/v1.2', '/.well-known']; + it('should validate x-forwarded-prefix with valid usage', () => { + const inputs = ['/foo', '/foo/baz', '/v1', '/app_name', '/', '/my-app']; for (const prefix of inputs) { const request = new Request('https://example.com', { @@ -186,122 +189,85 @@ describe('Validation Utils', () => { }, }); - expect(() => validateRequest(request, allowedHosts)) + expect(() => validateRequest(request, allowedHosts, false)) .withContext(`Prefix: "${prefix}"`) .not.toThrow(); } }); }); - describe('cloneRequestAndPatchHeaders', () => { - const allowedHosts = new Set(['example.com', '*.valid.com']); - - it('should validate host header when accessed via get()', async () => { + describe('sanitizeRequestHeaders', () => { + it('should allow x-forwarded-host and x-forwarded-proto when undefined', () => { const req = new Request('http://example.com', { - headers: { 'host': 'evil.com' }, + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'proxy.com', + 'x-forwarded-proto': 'https', + }, }); - const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); - - expect(() => secured.headers.get('host')).toThrowError( - 'Header "host" with value "evil.com" is not allowed.', - ); - await expectAsync(onError).toBeResolvedTo( - jasmine.objectContaining({ - message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed'), - }), + const { request: secured } = sanitizeRequestHeaders( + req, + normalizeTrustProxyHeaders(undefined), ); - }); - it('should allow valid host header', () => { - const req = new Request('http://example.com', { - headers: { 'host': 'example.com' }, - }); - const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts); expect(secured.headers.get('host')).toBe('example.com'); + expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com'); + expect(secured.headers.get('x-forwarded-proto')).toBe('https'); }); - it('should validate x-forwarded-host header', async () => { + it('should set deoptToCSR when x-forwarded-prefix is present and undefined', () => { const req = new Request('http://example.com', { - headers: { 'x-forwarded-host': 'evil.com' }, + headers: { + 'x-forwarded-prefix': '/prefix', + }, }); - const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); + const { deoptToCSR } = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined)); - expect(() => secured.headers.get('x-forwarded-host')).toThrowError( - 'Header "x-forwarded-host" with value "evil.com" is not allowed.', - ); - await expectAsync(onError).toBeResolvedTo( - jasmine.objectContaining({ - message: jasmine.stringMatching( - 'Header "x-forwarded-host" with value "evil.com" is not allowed', - ), - }), - ); + expect(deoptToCSR).toBeTrue(); }); - it('should allow accessing other headers without validation', () => { + it('should retain allowed proxy headers when explicitly provided', () => { + const trustProxyHeaders = new Set(['x-forwarded-host']); const req = new Request('http://example.com', { - headers: { 'accept': 'application/json' }, + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'proxy.com', + 'x-forwarded-proto': 'https', + }, }); - const { request: secured } = cloneRequestAndPatchHeaders(req, allowedHosts); + const { request: secured } = sanitizeRequestHeaders(req, trustProxyHeaders); - expect(secured.headers.get('accept')).toBe('application/json'); + expect(secured.headers.get('host')).toBe('example.com'); + expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com'); + expect(secured.headers.has('x-forwarded-proto')).toBeFalse(); }); - it('should validate headers when iterating with entries()', async () => { + it('should retain all proxy headers when trustProxyHeaders is true', () => { const req = new Request('http://example.com', { - headers: { 'host': 'evil.com' }, + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'proxy.com', + 'x-forwarded-proto': 'https', + }, }); - const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); - - expect(() => { - for (const _ of secured.headers.entries()) { - // access the header to trigger the validation - } - }).toThrowError('Header "host" with value "evil.com" is not allowed.'); - - await expectAsync(onError).toBeResolvedTo( - jasmine.objectContaining({ - message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), - }), - ); - }); + const { request: secured } = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(true)); - it('should validate headers when iterating with values()', async () => { - const req = new Request('http://example.com', { - headers: { 'host': 'evil.com' }, - }); - const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); - - expect(() => { - for (const _ of secured.headers.values()) { - // access the header to trigger the validation - } - }).toThrowError('Header "host" with value "evil.com" is not allowed.'); - - await expectAsync(onError).toBeResolvedTo( - jasmine.objectContaining({ - message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), - }), - ); + expect(secured.headers.get('host')).toBe('example.com'); + expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com'); + expect(secured.headers.get('x-forwarded-proto')).toBe('https'); }); - it('should validate headers when iterating with for...of', async () => { + it('should not clone the request if no proxy headers need to be removed', () => { const req = new Request('http://example.com', { - headers: { 'host': 'evil.com' }, + headers: { 'accept': 'application/json' }, }); - const { request: secured, onError } = cloneRequestAndPatchHeaders(req, allowedHosts); - - expect(() => { - for (const _ of secured.headers) { - // access the header to trigger the validation - } - }).toThrowError('Header "host" with value "evil.com" is not allowed.'); - - await expectAsync(onError).toBeResolvedTo( - jasmine.objectContaining({ - message: jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), - }), + const { request: secured } = sanitizeRequestHeaders( + req, + normalizeTrustProxyHeaders(undefined), ); + + expect(secured).toBe(req); + expect(secured.headers.get('accept')).toBe('application/json'); }); }); }); From 24e2502e1bfe4478f79e3e25d51c807a8aee8640 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 27 Apr 2026 06:25:21 +0000 Subject: [PATCH 24/26] refactor: ensure prerender routes start with a forward slash and update package resolution to use createRequire This fixes broken e2e tests --- .../build_angular/src/builders/app-shell/index.ts | 3 ++- .../build_angular/src/builders/prerender/index.ts | 2 +- .../build_angular/src/tools/webpack/utils/helpers.ts | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts index 1cdcd213c055..2772a6e1fa9b 100644 --- a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts @@ -82,10 +82,11 @@ async function _renderUniversal( localeDirectory, ); + const route = options.route; let html: string = await renderWorker.run({ serverBundlePath, document: indexHtml, - url: options.route, + url: route?.[0] === '/' ? route : '/' + route, }); // Overwrite the client index file. diff --git a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts index b56aa9df4b26..15896c8f2db3 100644 --- a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts @@ -209,7 +209,7 @@ async function _renderUniversal( inlineCriticalCss: !!normalizedStylesOptimization.inlineCritical, minifyCss: !!normalizedStylesOptimization.minify, outputPath, - route, + route: route[0] === '/' ? route : '/' + route, serverBundlePath, }; diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts b/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts index f57708663e03..72cbf96319d7 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts @@ -9,6 +9,7 @@ import type { ObjectPattern } from 'copy-webpack-plugin'; import glob from 'fast-glob'; import { createHash } from 'node:crypto'; +import { createRequire } from 'node:module'; import * as path from 'node:path'; import type { Configuration, WebpackOptionsNormalized } from 'webpack'; import { @@ -314,7 +315,8 @@ export function getStatsOptions(verbose = false): WebpackStatsOptions { */ export function isPackageInstalled(root: string, name: string): boolean { try { - require.resolve(name, { paths: [root] }); + const resolve = createRequire(root + '/').resolve; + resolve(name); return true; } catch { From 5e01ef40eb87deda79d18654fc696b64d18bf889 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:58:30 +0000 Subject: [PATCH 25/26] fix(@angular-devkit/build-angular): upgrade postcss to 8.5.12 This addresses GHSA-qx2v-qp2m-jg93 Fixes: #33067 --- packages/angular/build/package.json | 2 +- .../THIRD_PARTY_LICENSES.txt.golden | 2 +- .../angular_devkit/build_angular/package.json | 2 +- pnpm-lock.yaml | 70 +++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index a0e52d876ad5..cc7abb24e8bb 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -54,7 +54,7 @@ "jsdom": "26.1.0", "less": "4.4.0", "ng-packagr": "20.3.0", - "postcss": "8.5.6", + "postcss": "8.5.12", "rxjs": "7.8.2", "vitest": "3.2.4" }, diff --git a/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden index afbe64cd3fc4..cf31c4979bcf 100644 --- a/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden +++ b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden @@ -449,7 +449,7 @@ License: MIT The MIT License (MIT) -Copyright 2013 Andrey Sitnik +Copyright 2013 Andrey Sitnik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 28c8fc10bd03..2b3acfdc12da 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -43,7 +43,7 @@ "ora": "8.2.0", "picomatch": "4.0.4", "piscina": "5.1.3", - "postcss": "8.5.6", + "postcss": "8.5.12", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4cac7ae7c1f..0a4490b43567 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -441,8 +441,8 @@ importers: specifier: 20.3.0 version: 20.3.0(@angular/compiler-cli@20.3.7(@angular/compiler@20.3.7)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2) postcss: - specifier: 8.5.6 - version: 8.5.6 + specifier: 8.5.12 + version: 8.5.12 rxjs: specifier: 7.8.2 version: 7.8.2 @@ -640,7 +640,7 @@ importers: version: 4.1.3 autoprefixer: specifier: 10.4.21 - version: 10.4.21(postcss@8.5.6) + version: 10.4.21(postcss@8.5.12) babel-loader: specifier: 10.0.0 version: 10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.28.0)) @@ -699,11 +699,11 @@ importers: specifier: 5.1.3 version: 5.1.3 postcss: - specifier: 8.5.6 - version: 8.5.6 + specifier: 8.5.12 + version: 8.5.12 postcss-loader: specifier: 8.1.1 - version: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)) + version: 8.1.1(postcss@8.5.12)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)) resolve-url-loader: specifier: 5.0.0 version: 5.0.0 @@ -7787,8 +7787,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -13368,14 +13368,14 @@ snapshots: atomic-sleep@1.0.0: {} - autoprefixer@10.4.21(postcss@8.5.6): + autoprefixer@10.4.21(postcss@8.5.12): dependencies: browserslist: 4.26.3 caniuse-lite: 1.0.30001750 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.12 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -13485,7 +13485,7 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.0.0 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.12 postcss-media-query-parser: 0.2.3 before-after-hook@4.0.0: {} @@ -14048,12 +14048,12 @@ snapshots: css-loader@7.1.2(webpack@5.105.0(esbuild@0.28.0)): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) + icss-utils: 5.1.0(postcss@8.5.12) + postcss: 8.5.12 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.12) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.12) + postcss-modules-scope: 3.2.1(postcss@8.5.12) + postcss-modules-values: 4.0.0(postcss@8.5.12) postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: @@ -15582,9 +15582,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.6): + icss-utils@5.1.0(postcss@8.5.12): dependencies: - postcss: 8.5.6 + postcss: 8.5.12 idb@7.1.1: {} @@ -16671,7 +16671,7 @@ snapshots: less: 4.4.0 ora: 8.2.0 piscina: 5.1.3 - postcss: 8.5.6 + postcss: 8.5.12 rollup-plugin-dts: 6.2.1(rollup@4.59.0)(typescript@5.9.2) rxjs: 7.8.2 sass: 1.90.0 @@ -17161,11 +17161,11 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)): + postcss-loader@8.1.1(postcss@8.5.12)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)): dependencies: cosmiconfig: 9.0.0(typescript@5.9.2) jiti: 1.21.7 - postcss: 8.5.6 + postcss: 8.5.12 semver: 7.7.2 optionalDependencies: webpack: 5.105.0(esbuild@0.28.0) @@ -17174,26 +17174,26 @@ snapshots: postcss-media-query-parser@0.2.3: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.6): + postcss-modules-extract-imports@3.1.0(postcss@8.5.12): dependencies: - postcss: 8.5.6 + postcss: 8.5.12 - postcss-modules-local-by-default@4.2.0(postcss@8.5.6): + postcss-modules-local-by-default@4.2.0(postcss@8.5.12): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.12) + postcss: 8.5.12 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.6): + postcss-modules-scope@3.2.1(postcss@8.5.12): dependencies: - postcss: 8.5.6 + postcss: 8.5.12 postcss-selector-parser: 7.1.0 - postcss-modules-values@4.0.0(postcss@8.5.6): + postcss-modules-values@4.0.0(postcss@8.5.12): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.12) + postcss: 8.5.12 postcss-selector-parser@7.1.0: dependencies: @@ -17202,7 +17202,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -17557,7 +17557,7 @@ snapshots: adjust-sourcemap-loader: 4.0.0 convert-source-map: 1.9.0 loader-utils: 2.0.4 - postcss: 8.5.6 + postcss: 8.5.12 source-map: 0.6.1 resolve@1.22.10: @@ -18795,7 +18795,7 @@ snapshots: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.6 + postcss: 8.5.12 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: From 985e58075fb48c8cc1bc0d3075d2e1d41fe31433 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Wed, 29 Apr 2026 12:54:38 -0700 Subject: [PATCH 26/26] release: cut the v20.3.25 release --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c093b0c4a00..f2d75bb4a0eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + + +# 20.3.25 (2026-04-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [5e01ef40e](https://github.com/angular/angular-cli/commit/5e01ef40eb87deda79d18654fc696b64d18bf889) | fix | upgrade postcss to 8.5.12 | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------------- | +| [6686848d9](https://github.com/angular/angular-cli/commit/6686848d946ca157f9b92b84db377e912266395e) | fix | introduce trustProxyHeaders option to safely validate and sanitize proxy headers | + + + # 20.3.24 (2026-04-15) diff --git a/package.json b/package.json index c12f9964b448..105e77fb4d40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.24", + "version": "20.3.25", "private": true, "description": "Software Development Kit for Angular", "keywords": [