Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/reject-prefixed-server-env-keys.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions examples/start-env/env.serverprefix.ts
Original file line number Diff line number Diff line change
@@ -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),
},
};
6 changes: 6 additions & 0 deletions examples/start-env/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
21 changes: 21 additions & 0 deletions src/start-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading