Skip to content

Commit f836be9

Browse files
committed
fix(@angular/build): support Vite allowedHosts option for development server
Vite version 6.0.9+, which is now used by the Angular CLI, 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 newly introduced `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/build:dev-server", "options": { "allowedHosts": ["example.com"] }, ``` Additional details on the vulnerability can be found here: GHSA-vg6x-rcgg-rjx6
1 parent eb98bbd commit f836be9

File tree

8 files changed

+162
-4
lines changed

8 files changed

+162
-4
lines changed

goldens/public-api/angular/build/index.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export enum BuildOutputFileType {
108108

109109
// @public
110110
export type DevServerBuilderOptions = {
111+
allowedHosts?: AllowedHosts;
111112
buildTarget: string;
112113
headers?: {
113114
[key: string]: string;

packages/angular/build/src/builders/dev-server/options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export async function normalizeOptions(
103103
sslCert,
104104
sslKey,
105105
prebundle,
106+
allowedHosts,
106107
} = options;
107108

108109
// Return all the normalized options
@@ -128,5 +129,6 @@ export async function normalizeOptions(
128129
// Prebundling defaults to true but requires caching to function
129130
prebundle: cacheOptions.enabled && !optimization.scripts && prebundle,
130131
inspect,
132+
allowedHosts: allowedHosts ? allowedHosts : [],
131133
};
132134
}

packages/angular/build/src/builders/dev-server/schema.json

+17
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@
3636
"type": "string",
3737
"description": "SSL certificate to use for serving HTTPS."
3838
},
39+
"allowedHosts": {
40+
"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",
41+
"default": [],
42+
"oneOf": [
43+
{
44+
"type": "array",
45+
"description": "List of hosts that are allowed to access the development server.",
46+
"items": {
47+
"type": "string"
48+
}
49+
},
50+
{
51+
"type": "boolean",
52+
"description": "Indicates that all hosts are allowed. This is not recommended and a security risk."
53+
}
54+
]
55+
},
3956
"headers": {
4057
"type": "object",
4158
"description": "Custom HTTP headers to be added to all responses.",

packages/angular/build/src/builders/dev-server/tests/execute-fetch.ts

+48-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
*/
88

99
import { lastValueFrom, mergeMap, take, timeout } from 'rxjs';
10-
import { URL } from 'url';
10+
import { get, IncomingMessage, RequestOptions } from 'node:http';
11+
import { text } from 'node:stream/consumers';
1112
import {
1213
BuilderHarness,
1314
BuilderHarnessExecutionOptions,
@@ -41,3 +42,49 @@ export async function executeOnceAndFetch<T>(
4142
),
4243
);
4344
}
45+
46+
/**
47+
* Executes the builder and then immediately performs a GET request
48+
* via the Node.js `http` builtin module. This is useful for cases
49+
* where the `fetch` API is limited such as testing different `Host`
50+
* header values with the development server.
51+
* The `fetch` based alternative is preferred otherwise.
52+
*
53+
* @param harness A builder harness instance.
54+
* @param url The URL string to get.
55+
* @param options An options object.
56+
* @returns
57+
*/
58+
export async function executeOnceAndGet<T>(
59+
harness: BuilderHarness<T>,
60+
url: string,
61+
options?: Partial<BuilderHarnessExecutionOptions> & { request?: RequestOptions },
62+
): Promise<BuilderHarnessExecutionResult & { response?: IncomingMessage; content?: string }> {
63+
return lastValueFrom(
64+
harness.execute().pipe(
65+
timeout(30000),
66+
mergeMap(async (executionResult) => {
67+
let response = undefined;
68+
let content = undefined;
69+
if (executionResult.result?.success) {
70+
let baseUrl = `${executionResult.result.baseUrl}`;
71+
baseUrl = baseUrl[baseUrl.length - 1] === '/' ? baseUrl : `${baseUrl}/`;
72+
const resolvedUrl = new URL(url, baseUrl);
73+
74+
response = await new Promise<IncomingMessage>((resolve) =>
75+
get(resolvedUrl, options?.request ?? {}, resolve),
76+
);
77+
78+
if (response.statusCode === 200) {
79+
content = await text(response);
80+
}
81+
82+
response.resume();
83+
}
84+
85+
return { ...executionResult, response, content };
86+
}),
87+
take(1),
88+
),
89+
);
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { executeDevServer } from '../../index';
10+
import { executeOnceAndGet } from '../execute-fetch';
11+
import { describeServeBuilder } from '../jasmine-helpers';
12+
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
13+
14+
const FETCH_HEADERS = Object.freeze({ Host: 'example.com' });
15+
16+
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
17+
describe('option: "allowedHosts"', () => {
18+
beforeEach(async () => {
19+
setupTarget(harness);
20+
21+
// Application code is not needed for these tests
22+
await harness.writeFile('src/main.ts', '');
23+
});
24+
25+
it('does not allow an invalid host when option is not present', async () => {
26+
harness.useTarget('serve', {
27+
...BASE_OPTIONS,
28+
});
29+
30+
const { result, response } = await executeOnceAndGet(harness, '/', {
31+
request: { headers: FETCH_HEADERS },
32+
});
33+
34+
expect(result?.success).toBeTrue();
35+
expect(response?.statusCode).toBe(403);
36+
});
37+
38+
it('does not allow an invalid host when option is an empty array', async () => {
39+
harness.useTarget('serve', {
40+
...BASE_OPTIONS,
41+
allowedHosts: [],
42+
});
43+
44+
const { result, response } = await executeOnceAndGet(harness, '/', {
45+
request: { headers: FETCH_HEADERS },
46+
});
47+
48+
expect(result?.success).toBeTrue();
49+
expect(response?.statusCode).toBe(403);
50+
});
51+
52+
it('allows a host when specified in the option', async () => {
53+
harness.useTarget('serve', {
54+
...BASE_OPTIONS,
55+
allowedHosts: ['example.com'],
56+
});
57+
58+
const { result, content } = await executeOnceAndGet(harness, '/', {
59+
request: { headers: FETCH_HEADERS },
60+
});
61+
62+
expect(result?.success).toBeTrue();
63+
expect(content).toContain('<title>');
64+
});
65+
66+
it('allows a host when option is true', async () => {
67+
harness.useTarget('serve', {
68+
...BASE_OPTIONS,
69+
allowedHosts: true,
70+
});
71+
72+
const { result, content } = await executeOnceAndGet(harness, '/', {
73+
request: { headers: FETCH_HEADERS },
74+
});
75+
76+
expect(result?.success).toBeTrue();
77+
expect(content).toContain('<title>');
78+
});
79+
});
80+
});

packages/angular/build/src/builders/dev-server/vite-server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ export async function setupServer(
758758
strictPort: true,
759759
host: serverOptions.host,
760760
open: serverOptions.open,
761+
allowedHosts: serverOptions.allowedHosts,
761762
headers: serverOptions.headers,
762763
// Disable the websocket if live reload is disabled (false/undefined are the only valid values)
763764
ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined,

packages/angular_devkit/build_angular/src/builders/dev-server/builder.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,22 @@ export function execute(
8888
// New build system defaults hmr option to the value of liveReload
8989
normalizedOptions.hmr ??= normalizedOptions.liveReload;
9090

91+
// New build system uses Vite's allowedHost option convention of true for disabling host checks
92+
if (normalizedOptions.disableHostCheck) {
93+
(normalizedOptions as unknown as { allowedHosts: true }).allowedHosts = true;
94+
} else {
95+
normalizedOptions.allowedHosts ??= [];
96+
}
97+
9198
return defer(() =>
9299
Promise.all([import('@angular/build/private'), import('../browser-esbuild')]),
93100
).pipe(
94101
switchMap(([{ serveWithVite, buildApplicationInternal }, { convertBrowserOptions }]) =>
95102
serveWithVite(
96-
normalizedOptions as typeof normalizedOptions & { hmr: boolean },
103+
normalizedOptions as typeof normalizedOptions & {
104+
hmr: boolean;
105+
allowedHosts: true | string[];
106+
},
97107
builderName,
98108
(options, context, codePlugins) => {
99109
return builderName === '@angular-devkit/build-angular:browser-esbuild'

packages/angular_devkit/build_angular/src/builders/dev-server/schema.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
},
6868
"allowedHosts": {
6969
"type": "array",
70-
"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.",
70+
"description": "List of hosts that are allowed to access the dev server.",
7171
"default": [],
7272
"items": {
7373
"type": "string"
@@ -79,7 +79,7 @@
7979
},
8080
"disableHostCheck": {
8181
"type": "boolean",
82-
"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.",
82+
"description": "Don't verify connected clients are part of allowed hosts.",
8383
"default": false
8484
},
8585
"hmr": {

0 commit comments

Comments
 (0)