From fdddf2c0844081667a09f2ffe0b16f77384959b2 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:30:23 -0500 Subject: [PATCH] fix(@angular/build): update vite to version 5.4.14 Version update from 5.4.6 to address advisory https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6 Vite version 5.4.12+, which is now used by the Angular CLI with the `application`/`browser-esbuild` builders, contains a potentially breaking change for some development setups. Examples of such setups include those that use reverse proxies or custom host names during development. The change within a patch release was made by Vite to address a security vulnerability. For projects that directly access the development server via `localhost`, no changes should be needed. However, some development setups may now need to adjust the `allowedHosts` development server option. This option can include an array of host names that are allowed to communicate with the development server. The option sets the corresponding Vite option within the Angular CLI. For more information on the option and its specific behavior, please see the Vite documentation located here: https://vite.dev/config/server-options.html#server-allowedhosts The following is an example of the configuration option allowing `example.com`: ``` "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { "allowedHosts": ["example.com"] }, ``` --- goldens/public-api/angular/build/index.api.md | 1 + package.json | 2 +- packages/angular/build/package.json | 2 +- .../build/src/builders/dev-server/options.ts | 2 + .../build/src/builders/dev-server/schema.json | 17 ++++ .../dev-server/tests/execute-fetch.ts | 48 ++++++++++- .../tests/options/allowed-hosts_spec.ts | 80 +++++++++++++++++++ .../src/builders/dev-server/vite-server.ts | 1 + .../src/builders/dev-server/builder.ts | 11 ++- .../src/builders/dev-server/schema.json | 4 +- yarn.lock | 47 ++++++++++- 11 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index c3efe5dda318..443a55279ab2 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -110,6 +110,7 @@ export enum BuildOutputFileType { // @public export interface DevServerBuilderOptions { + allowedHosts?: AllowedHosts; buildTarget: string; headers?: { [key: string]: string; diff --git a/package.json b/package.json index 29acd802c7d0..a296832c7752 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,7 @@ "undici": "6.19.7", "verdaccio": "5.32.1", "verdaccio-auth-memory": "^10.0.0", - "vite": "5.4.6", + "vite": "5.4.14", "watchpack": "2.4.1", "webpack": "5.94.0", "webpack-dev-middleware": "7.4.2", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index f8b94876401e..0f420f8f4ca4 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -41,7 +41,7 @@ "rollup": "4.22.4", "sass": "1.77.6", "semver": "7.6.3", - "vite": "5.4.6", + "vite": "5.4.14", "watchpack": "2.4.1" }, "peerDependencies": { diff --git a/packages/angular/build/src/builders/dev-server/options.ts b/packages/angular/build/src/builders/dev-server/options.ts index 080e168699bc..cb6dd438ad6f 100644 --- a/packages/angular/build/src/builders/dev-server/options.ts +++ b/packages/angular/build/src/builders/dev-server/options.ts @@ -103,6 +103,7 @@ export async function normalizeOptions( sslCert, sslKey, prebundle, + allowedHosts, } = options; // Return all the normalized options @@ -128,5 +129,6 @@ export async function normalizeOptions( // Prebundling defaults to true but requires caching to function prebundle: cacheOptions.enabled && !optimization.scripts && prebundle, inspect, + allowedHosts: allowedHosts ? allowedHosts : [], }; } diff --git a/packages/angular/build/src/builders/dev-server/schema.json b/packages/angular/build/src/builders/dev-server/schema.json index 3adce45eb71a..775ce72ea4d4 100644 --- a/packages/angular/build/src/builders/dev-server/schema.json +++ b/packages/angular/build/src/builders/dev-server/schema.json @@ -36,6 +36,23 @@ "type": "string", "description": "SSL certificate to use for serving HTTPS." }, + "allowedHosts": { + "description": "The hosts that can access the development server. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts", + "default": [], + "oneOf": [ + { + "type": "array", + "description": "List of hosts that are allowed to access the development server.", + "items": { + "type": "string" + } + }, + { + "type": "boolean", + "description": "Indicates that all hosts are allowed. This is not recommended and a security risk." + } + ] + }, "headers": { "type": "object", "description": "Custom HTTP headers to be added to all responses.", diff --git a/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts b/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts index 3bb731a6c6b3..a36196da14be 100644 --- a/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts +++ b/packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts @@ -7,7 +7,8 @@ */ import { lastValueFrom, mergeMap, take, timeout } from 'rxjs'; -import { URL } from 'url'; +import { get, IncomingMessage, RequestOptions } from 'node:http'; +import { text } from 'node:stream/consumers'; import { BuilderHarness, BuilderHarnessExecutionOptions, @@ -41,3 +42,48 @@ export async function executeOnceAndFetch( ), ); } + +/** + * Executes the builder and then immediately performs a GET request + * via the Node.js `http` builtin module. This is useful for cases + * where the `fetch` API is limited such as testing different `Host` + * header values with the development server. + * The `fetch` based alternative is preferred otherwise. + * + * @param harness A builder harness instance. + * @param url The URL string to get. + * @param options An options object. + */ +export async function executeOnceAndGet( + harness: BuilderHarness, + url: string, + options?: Partial & { request?: RequestOptions }, +): Promise { + return lastValueFrom( + harness.execute().pipe( + timeout(30_000), + mergeMap(async (executionResult) => { + let response = undefined; + let content = undefined; + if (executionResult.result?.success) { + let baseUrl = `${executionResult.result.baseUrl}`; + baseUrl = baseUrl[baseUrl.length - 1] === '/' ? baseUrl : `${baseUrl}/`; + const resolvedUrl = new URL(url, baseUrl); + + response = await new Promise((resolve) => + get(resolvedUrl, options?.request ?? {}, resolve), + ); + + if (response.statusCode === 200) { + content = await text(response); + } + + response.resume(); + } + + return { ...executionResult, response, content }; + }), + take(1), + ), + ); +} diff --git a/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts new file mode 100644 index 000000000000..8e96c7b4b4b0 --- /dev/null +++ b/packages/angular/build/src/builders/dev-server/tests/options/allowed-hosts_spec.ts @@ -0,0 +1,80 @@ +/** + * @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 { executeDevServer } from '../../index'; +import { executeOnceAndGet } from '../execute-fetch'; +import { describeServeBuilder } from '../jasmine-helpers'; +import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup'; + +const FETCH_HEADERS = Object.freeze({ Host: 'example.com' }); + +describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => { + describe('option: "allowedHosts"', () => { + beforeEach(async () => { + setupTarget(harness); + + // Application code is not needed for these tests + await harness.writeFile('src/main.ts', ''); + }); + + it('does not allow an invalid host when option is not present', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(response?.statusCode).toBe(403); + }); + + it('does not allow an invalid host when option is an empty array', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + allowedHosts: [], + }); + + const { result, response } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(response?.statusCode).toBe(403); + }); + + it('allows a host when specified in the option', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + allowedHosts: ['example.com'], + }); + + const { result, content } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(content).toContain(''); + }); + + it('allows a host when option is true', async () => { + harness.useTarget('serve', { + ...BASE_OPTIONS, + allowedHosts: true, + }); + + const { result, content } = await executeOnceAndGet(harness, '/', { + request: { headers: FETCH_HEADERS }, + }); + + expect(result?.success).toBeTrue(); + expect(content).toContain('<title>'); + }); + }); +}); 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 f6a4f54accd2..b005deee8dce 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -551,6 +551,7 @@ export async function setupServer( strictPort: true, host: serverOptions.host, open: serverOptions.open, + allowedHosts: serverOptions.allowedHosts, headers: serverOptions.headers, proxy, cors: { diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts index 3b244a008c2c..f99bb5c3d6c8 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts @@ -85,12 +85,21 @@ export function execute( ); } + // New build system uses Vite's allowedHost option convention of true for disabling host checks + if (normalizedOptions.disableHostCheck) { + (normalizedOptions as unknown as { allowedHosts: true }).allowedHosts = true; + } else { + normalizedOptions.allowedHosts ??= []; + } + return defer(() => Promise.all([import('@angular/build/private'), import('../browser-esbuild')]), ).pipe( switchMap(([{ serveWithVite, buildApplicationInternal }, { convertBrowserOptions }]) => serveWithVite( - normalizedOptions, + normalizedOptions as typeof normalizedOptions & { + allowedHosts: true | string[]; + }, builderName, (options, context, codePlugins) => { return builderName === '@angular-devkit/build-angular:browser-esbuild' diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json b/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json index 5796dd04e895..ce8242b234dc 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/schema.json @@ -73,7 +73,7 @@ }, "allowedHosts": { "type": "array", - "description": "List of hosts that are allowed to access the dev server. This option has no effect when using the 'application' or other esbuild-based builders.", + "description": "List of hosts that are allowed to access the dev server.", "default": [], "items": { "type": "string" @@ -85,7 +85,7 @@ }, "disableHostCheck": { "type": "boolean", - "description": "Don't verify connected clients are part of allowed hosts. This option has no effect when using the 'application' or other esbuild-based builders.", + "description": "Don't verify connected clients are part of allowed hosts.", "default": false }, "hmr": { diff --git a/yarn.lock b/yarn.lock index 9303c206eea5..d8a69a7091be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -401,7 +401,7 @@ __metadata: rollup: "npm:4.22.4" sass: "npm:1.77.6" semver: "npm:7.6.3" - vite: "npm:5.4.6" + vite: "npm:5.4.14" watchpack: "npm:2.4.1" peerDependencies: "@angular/compiler-cli": ^18.0.0 @@ -805,7 +805,7 @@ __metadata: undici: "npm:6.19.7" verdaccio: "npm:5.32.1" verdaccio-auth-memory: "npm:^10.0.0" - vite: "npm:5.4.6" + vite: "npm:5.4.14" watchpack: "npm:2.4.1" webpack: "npm:5.94.0" webpack-dev-middleware: "npm:7.4.2" @@ -18053,6 +18053,49 @@ __metadata: languageName: node linkType: hard +"vite@npm:5.4.14": + version: 5.4.14 + resolution: "vite@npm:5.4.14" + dependencies: + esbuild: "npm:^0.21.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.43" + rollup: "npm:^4.20.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/8842933bd70ca6a98489a0bb9c8464bec373de00f9a97c8c7a4e64b24d15c88bfaa8c1acb38a68c3e5eb49072ffbccb146842c2d4edcdd036a9802964cffe3d1 + languageName: node + linkType: hard + "vite@npm:5.4.6": version: 5.4.6 resolution: "vite@npm:5.4.6"