Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions apps/worker/src/commands/__tests__/utils.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions apps/worker/src/commands/utils/env-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ const ENV_VARS_END = `# END ${PRODUCT_NAME} environment variables`;
/** POSIX-compliant env var name: letters, digits, underscores; must not start with a digit. */
const VALID_ENV_VAR_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;

/**
* Characters Vite treats as reserved in __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS.
* A single occurrence makes Vite skip the entire variable.
*/
const VITE_UNSAFE_ALLOWED_HOST_CHARS = /["'\\]/;

/**
* Returns true when `name` is a safe POSIX environment variable name.
* Rejects names containing shell metacharacters that could lead to command
Expand Down Expand Up @@ -195,6 +201,8 @@ export async function injectEnvVars(
const previewProxySubdomainSuffix =
options?.previewProxySubdomainSuffix ??
process.env.PREVIEW_PROXY_SUBDOMAIN_SUFFIX;
const previewHostnames: string[] = [];
const machineHostnames: string[] = [];

if (identity && identity.taskId && previewProxyBaseUrl) {
for (const [name, domain] of Object.entries(identity.machineDomains)) {
Expand All @@ -216,6 +224,17 @@ export async function injectEnvVars(
delete envVars[previewUrlEnvVarName];
} else {
envVars[previewUrlEnvVarName] = previewUrl;
previewHostnames.push(new URL(previewUrl).hostname);

// Requests that arrive through the preview proxy reach the dev server
// with the sandbox's own host (http-proxy rewrites Host when it changes
// origin), and unproxied ports are browsed at that host directly.
try {
machineHostnames.push(new URL(domain).hostname);
} catch {
// Providers that report a bare host rather than a URL have nothing
// extra to allowlist beyond the preview hostname above.
}
}

if (isProxied) {
Expand All @@ -230,6 +249,28 @@ export async function injectEnvVars(
}
}

// Vite rejects requests and HMR WebSocket upgrades whose Host header is not
// in `server.allowedHosts` (Astro, Nuxt and SvelteKit inherit this). Two
// different hosts reach a sandbox dev server: the sandbox's own host on plain
// HTTP, because the preview proxy changes origin, and the public preview host
// on upgrades, because the auth-proxy rewrites Host there. Allowlisting only
// the first is why previews render but HMR dies with a 400. Vite reads this
// variable when it resolves config and appends it to allowedHosts, so setting
// both fixes HMR without every repo hardcoding a preview domain of its own.
// Pin exact hostnames rather than a wildcard suffix, and let an explicit
// deployment-provided value win.
if (!envVars.__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS) {
// Vite discards the whole list when it contains quoting metacharacters;
// hostnames never do, so drop anything malformed instead of losing the rest.
const allowedHosts = [
...new Set([...previewHostnames, ...machineHostnames]),
].filter((hostname) => !VITE_UNSAFE_ALLOWED_HOST_CHARS.test(hostname));

if (allowedHosts.length > 0) {
envVars.__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS = allowedHosts.join(',');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vite treats __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS as one host, not a comma-separated list: its resolver appends the entire environment value as a single allowedHosts entry. With the normal preview and sandbox hostnames, this produces task-123-web.preview...,sandbox-web... as one value, which matches neither Host header, so the HMR upgrade remains rejected. The new tests only split the value themselves and therefore do not exercise Vite's parsing behavior.

}
}

// Dev servers running in the sandbox (e.g. Next.js) reject cross-origin
// requests to internal dev resources (fonts, HMR, overlay assets) unless the
// preview hostname is allowlisted; apps derive that allowlist from
Expand Down
Loading