Skip to main content

Only PUBLIC_-prefixed env vars referenced from client code

ab-002567 · project-snapshot.secrets.public-prefix-only-in-client
Severity: highactive

Why it matters

Client-side bundles are shipped to every visitor's browser and are trivially readable via devtools — anything inlined at build time is public. When a 'use client' component references process.env.STRIPE_SECRET_KEY, Next.js, Vite, and Astro all silently replace the unprefixed reference with undefined at bundle time, which feels like a safety net but isn't: the moment someone renames the var to match the public prefix (often to fix a "why is this undefined?" bug), the secret ships to the browser. AI coding tools are particularly prone to this pattern because they don't reliably distinguish server from client context and often reach for process.env by habit on both sides of the boundary. The check catches the architectural mistake before it becomes a leak.

Severity rationale

High because the current runtime behavior (undefined substitution) only hides the bug, not the design flaw — one rename during debugging promotes the issue to an instant credential leak in the next deploy.

Remediation

Anything safe to expose to the browser (publishable keys, public URLs) should be prefixed. Anything sensitive (secret keys, database URLs, service-role keys) must NEVER be referenced client-side — move that logic to a server action, route handler, or API route.

Deeper remediation guidance and cross-reference coverage for this check lives in the security-hardening Pro audit — run that after applying this fix for a more exhaustive pass on the same topic.

Detection

  • ID: project-snapshot.secrets.public-prefix-only-in-client
  • Severity: high
  • What to look for: Enumerate every process.env.X reference in client-side files. Client-side files include any .tsx/.jsx file that does not start with 'use server', anything in app/_components/* or components/* that is rendered on the client, and anything imported from those. For Next.js, the public prefix is NEXT_PUBLIC_; Vite uses VITE_; Astro uses PUBLIC_. Count every reference and classify each as prefixed or unprefixed.
  • Pass criteria: 100% of process.env.X references in client code use the framework's public prefix (NEXT_PUBLIC_/VITE_/PUBLIC_).
  • Fail criteria: At least one reference to an unprefixed env var in client code. Even one is a fail — bundlers will inline undefined at runtime, but the leak risk if it WERE defined is high.
  • Skip (N/A) when: No client-side framework detected (pure backend project).
  • Do NOT pass when: A 'use client' file references process.env.STRIPE_SECRET_KEY or any obvious server-only var, even if that var is currently unset.
  • Report even on pass: "Found N process.env references in client files; N use the public prefix (NEXT_PUBLIC_/VITE_/PUBLIC_)."
  • Detail on fail: "3 unprefixed env vars referenced from client files: STRIPE_SECRET_KEY in app/checkout/page.tsx, DATABASE_URL in components/db-status.tsx, ...".
  • Remediation: Anything safe to expose to the browser (publishable keys, public URLs) should be prefixed. Anything sensitive (secret keys, database URLs, service-role keys) must NEVER be referenced client-side — move that logic to a server action, route handler, or API route.

Taxons

History