diff --git a/.changeset/grumpy-sloths-fail.md b/.changeset/grumpy-sloths-fail.md new file mode 100644 index 000000000000..1a5e9b272b76 --- /dev/null +++ b/.changeset/grumpy-sloths-fail.md @@ -0,0 +1,30 @@ +--- +'astro': minor +--- + +Adds a new configuration option `server.allowedHosts` and CLI option `--allowed-hosts`. + +Now you can specify the hostnames that the dev and preview servers are allowed to respond to. This is useful for allowing additional subdomains, or running the dev server in a web container. + +`allowedHosts` checks the Host header on HTTP requests from browsers and if it doesn't match, it will reject the request to prevent CSRF and XSS attacks. + +```shell +astro dev --allowed-hosts=foo.bar.example.com,bar.example.com +``` + +```shell +astro preview --allowed-hosts=foo.bar.example.com,bar.example.com +``` + +```js +// astro.config.mjs +import {defineConfig} from "astro/config"; + +export default defineConfig({ + server: { + allowedHosts: ['foo.bar.example.com', 'bar.example.com'] + } +}) +``` + +This feature is the same as [Vite's `server.allowHosts` configuration](https://vite.dev/config/server-options.html#server-allowedhosts). diff --git a/packages/astro/src/cli/dev/index.ts b/packages/astro/src/cli/dev/index.ts index 4bf888c430f6..d10d000592cf 100644 --- a/packages/astro/src/cli/dev/index.ts +++ b/packages/astro/src/cli/dev/index.ts @@ -20,6 +20,7 @@ export async function dev({ flags }: DevOptions) { ['--host ', `Expose on a network IP address at `], ['--open', 'Automatically open the app in the browser on server start'], ['--force', 'Clear the content layer cache, forcing a full rebuild.'], + ['--allowed-hosts', 'Specify a comma-separated list of allowed hosts or allow any hostname.'], ['--help (-h)', 'See all available flags.'], ], }, diff --git a/packages/astro/src/cli/flags.ts b/packages/astro/src/cli/flags.ts index 7466fdda7aea..c283ec85e0c1 100644 --- a/packages/astro/src/cli/flags.ts +++ b/packages/astro/src/cli/flags.ts @@ -25,6 +25,12 @@ export function flagsToAstroInlineConfig(flags: Flags): AstroInlineConfig { typeof flags.host === 'string' || typeof flags.host === 'boolean' ? flags.host : undefined, open: typeof flags.open === 'string' || typeof flags.open === 'boolean' ? flags.open : undefined, + allowedHosts: + typeof flags.allowedHosts === 'string' + ? flags.allowedHosts.split(',') + : typeof flags.allowedHosts === 'boolean' && flags.allowedHosts === true + ? flags.allowedHosts + : [], }, }; } diff --git a/packages/astro/src/cli/preview/index.ts b/packages/astro/src/cli/preview/index.ts index 468332ce3b97..9e0b88e111b8 100644 --- a/packages/astro/src/cli/preview/index.ts +++ b/packages/astro/src/cli/preview/index.ts @@ -18,6 +18,7 @@ export async function preview({ flags }: PreviewOptions) { ['--host', `Listen on all addresses, including LAN and public addresses.`], ['--host ', `Expose on a network IP address at `], ['--open', 'Automatically open the app in the browser on server start'], + ['--allowed-hosts', 'Specify a comma-separated list of allowed hosts or allow any hostname.'], ['--help (-h)', 'See all available flags.'], ], }, diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 83546aeb9a9e..82609e3b498a 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -77,6 +77,7 @@ export const ASTRO_CONFIG_DEFAULTS = { host: false, port: 4321, open: false, + allowedHosts: [], }, integrations: [], markdown: markdownConfigDefaults, @@ -214,6 +215,10 @@ export const AstroConfigSchema = z.object({ .default(ASTRO_CONFIG_DEFAULTS.server.host), port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port), headers: z.custom().optional(), + allowedHosts: z + .union([z.array(z.string()), z.literal(true)]) + .optional() + .default(ASTRO_CONFIG_DEFAULTS.server.allowedHosts), }) .default({}), ), @@ -718,6 +723,10 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) { port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port), headers: z.custom().optional(), streaming: z.boolean().optional().default(true), + allowedHosts: z + .union([z.array(z.string()), z.literal(true)]) + .optional() + .default(ASTRO_CONFIG_DEFAULTS.server.allowedHosts), }) .optional() .default({}), diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts index d1570f4920cb..c984fae7d254 100644 --- a/packages/astro/src/core/dev/container.ts +++ b/packages/astro/src/core/dev/container.ts @@ -56,7 +56,7 @@ export async function createContainer({ const { base, - server: { host, headers, open: serverOpen }, + server: { host, headers, open: serverOpen, allowedHosts }, } = settings.config; // serverOpen = true, isRestart = false @@ -92,7 +92,7 @@ export async function createContainer({ const mode = inlineConfig?.mode ?? 'development'; const viteConfig = await createVite( { - server: { host, headers, open }, + server: { host, headers, open, allowedHosts }, optimizeDeps: { include: rendererClientEntries, }, diff --git a/packages/astro/src/core/preview/static-preview-server.ts b/packages/astro/src/core/preview/static-preview-server.ts index 855506ef91ce..13798ef99e2c 100644 --- a/packages/astro/src/core/preview/static-preview-server.ts +++ b/packages/astro/src/core/preview/static-preview-server.ts @@ -36,6 +36,7 @@ export default async function createStaticPreviewServer( port: settings.config.server.port, headers: settings.config.server.headers, open: settings.config.server.open, + allowedHosts: settings.config.server.allowedHosts }, plugins: [vitePluginAstroPreview(settings)], }); diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 84dc3fd83484..edcb70b164c3 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -68,6 +68,27 @@ export type ServerConfig = { */ port?: number; + + /** + * @name server.allowedHosts + * @type {string[] | true} + * @default `[]` + * @version 5.4.0 + * @description + * + * A list of hostnames that Astro is allowed to respond to. When the value is set to `true`, any + * hostname is allowed. + * + * ```js + * { + * server: { + * allowedHosts: ['staging.example.com', 'qa.example.com'] + * } + * } + * ``` + */ + allowedHosts?: string[] | true; + /** * @name server.headers * @typeraw {OutgoingHttpHeaders}