From 3cbd3f99814240cdd1485c341b6897be149d6e82 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Mon, 10 Aug 2026 22:29:57 -0500 Subject: [PATCH] =?UTF-8?q?fix(start-env):=20prefixed=20server=20keys=20ar?= =?UTF-8?q?e=20a=20config-time=20error=20=E2=80=94=20Vite=20bakes=20every?= =?UTF-8?q?=20VITE=5F-prefixed=20var=20into=20the=20browser's=20import.met?= =?UTF-8?q?a.env=20regardless=20of=20schema=20side,=20so=20a=20server-decl?= =?UTF-8?q?ared=20VITE=5F*=20secret=20leaked=20silently=20through=20Vite's?= =?UTF-8?q?=20own=20channel=20(the=20leak=20scan=20only=20watches=20the=20?= =?UTF-8?q?virtual=20server=20module's=20values);=20the=20prefix=20rule=20?= =?UTF-8?q?is=20now=20enforced=20both=20ways,=20with=20a=20fixture=20and?= =?UTF-8?q?=20guard=20assertion=20in=20the=20start-env=20suite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .changeset/reject-prefixed-server-env-keys.md | 5 +++++ examples/start-env/env.serverprefix.ts | 12 +++++++++++ examples/start-env/test/run.mjs | 6 ++++++ src/start-env.ts | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 .changeset/reject-prefixed-server-env-keys.md create mode 100644 examples/start-env/env.serverprefix.ts diff --git a/.changeset/reject-prefixed-server-env-keys.md b/.changeset/reject-prefixed-server-env-keys.md new file mode 100644 index 0000000..dd27b37 --- /dev/null +++ b/.changeset/reject-prefixed-server-env-keys.md @@ -0,0 +1,5 @@ +--- +'vite-plugin-solid': patch +--- + +`start.env` now rejects `server` schema keys that carry the public env prefix at config time. Vite bakes every `VITE_`-prefixed variable (or whatever `envPrefix` selects) into the browser's `import.meta.env` regardless of which side of the schema declares it, so `server: { VITE_API_SECRET: ... }` silently shipped the secret to every client through Vite's own channel — with no diagnostics, since the leak scan only watches the virtual server module's values. The prefix rule was previously enforced one-way (client keys must have it); the reverse guard now fails fast with a rename message, mirroring the existing client-side guard. diff --git a/examples/start-env/env.serverprefix.ts b/examples/start-env/env.serverprefix.ts new file mode 100644 index 0000000..33e4491 --- /dev/null +++ b/examples/start-env/env.serverprefix.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +// Fixture for the config-time server-prefix guard +// (ENV_SCHEMA=./env.serverprefix.ts): Vite bakes every VITE_-prefixed var +// into the browser's import.meta.env regardless of schema side, so a +// prefixed key under `server` can never stay secret and must be rejected +// before anything builds. +export default { + server: { + VITE_API_SECRET: z.string().min(8), + }, +}; diff --git a/examples/start-env/test/run.mjs b/examples/start-env/test/run.mjs index 8f93c48..464efb4 100644 --- a/examples/start-env/test/run.mjs +++ b/examples/start-env/test/run.mjs @@ -365,6 +365,12 @@ async function guardsMode() { out = await build({ ENV_SCHEMA: './env.badprefix.ts' }).catch((e) => e.message); record('guards', 'prefix', 'non-VITE_ client key is a config-time error', /must carry the public env prefix/.test(out) && /APP_NAME/.test(out), out.slice(0, 400)); + + // The reverse: Vite bakes every VITE_-prefixed var into the browser's + // import.meta.env regardless of schema side, so a prefixed SERVER key + // is a leak the moment it exists — reject it before anything builds. + out = await build({ ENV_SCHEMA: './env.serverprefix.ts' }).catch((e) => e.message); + record('guards', 'prefix', 'VITE_-prefixed server key is a config-time error', /cannot keep it secret/.test(out) && /VITE_API_SECRET/.test(out), out.slice(0, 400)); } async function prodMode() { diff --git a/src/start-env.ts b/src/start-env.ts index a6b8aab..e47351c 100644 --- a/src/start-env.ts +++ b/src/start-env.ts @@ -247,6 +247,27 @@ function assertSchemaShape( ); } } + // The reverse guard: Vite itself bakes every prefixed variable into + // `import.meta.env` for the browser, so declaring one under `server` + // cannot keep it secret — it leaks through Vite's channel with no + // diagnostics from this plugin's leak scan (which only watches the + // virtual server module's values). + for (const key of Object.keys(typed.server ?? {})) { + const prefix = envPrefixes.find((p) => key.startsWith(p)); + if (prefix) { + const bare = key.slice(prefix.length); + throw new Error( + `[vite-plugin-solid] server env var "${key}" in ${envFile} carries the public ` + + `env prefix "${prefix}". Vite exposes every "${prefix}"-prefixed variable to ` + + `the browser through import.meta.env no matter which side declares it, so a ` + + `\`server\` entry cannot keep it secret. ` + + (bare + ? `Rename it to "${bare}" (in the schema and in your .env/environment), or ` + : `Rename it without the prefix, or `) + + `move it to \`client\` if it is public.`, + ); + } + } return typed; }