DEV Community

Veristria
Veristria

Posted on Originally published at keydrift.dev

Your NEXT_PUBLIC secret is already in the browser bundle

Problem

In a Next.js app it’s easy to slip a secret (e.g., STRIPE_SECRET_KEY, OpenAI API key, Supabase JWT) into a client‑side bundle. When a server component reads process.env.STRIPE_SECRET_KEY it stays on the server, but copying that line into a client component causes the build to fail to resolve the variable. The common “quick fix”—renaming the variable with the NEXT_PUBLIC_ prefix—makes the value part of the JavaScript that every visitor downloads, turning a server‑only secret into a public leak.

How it works (mechanism)

  1. Next.js environment variable scoping

    • Variables without the NEXT_PUBLIC_ prefix are stripped from the client bundle at build time. They are only available in server‑side code (pages/api/*, server components, getServerSideProps, etc.).
    • Variables with the NEXT_PUBLIC_ prefix are injected into the client bundle and can be read from process.env in any browser‑executed code.
  2. Accidental exposure

    • A developer moves a line such as const stripeKey = process.env.STRIPE_SECRET_KEY; from a server component to a client component (or a shared utility imported by both).
    • The build fails because STRIPE_SECRET_KEY is undefined on the client.
    • To silence the error they rename the variable to NEXT_PUBLIC_STRIPE_SECRET_KEY.
    • The renamed variable is now inlined into the bundle, exposing the secret to anyone who can view the page source or network traffic.
  3. Why the leak is critical

    • Secrets like Stripe secret keys, OpenAI API keys, Supabase JWTs, AWS access keys, and webhook signing secrets grant full access to the respective services. Once they appear in a public bundle they can be harvested by bots or malicious actors.

Detection (KeyDrift)

KeyDrift performs a read‑only scan of your client bundle—no credentials are required—to locate hard‑coded secrets and environment variables that have been inlined. The scan cross‑references each detected credential with the tool that introduced it (e.g., Next.js, Replit, Cursor).

Typical output for a Next.js leak looks like:

[Critical] STRIPE_SECRET_KEY found in client bundle (tool: Next.js)
Location: static/chunks/pages/_app.js:1234
Recommendation: Move usage to a server component or API route.
Enter fullscreen mode Exit fullscreen mode

KeyDrift also flags variables that have been renamed with the NEXT_PUBLIC_ prefix and marks them as high or critical depending on the credential type (e.g., Stripe secret key → critical).

Fix

1. Keep the secret on the server

// app/api/stripe/checkout/route.ts (server‑only)
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

export async function POST(req: Request) {
  // server‑side logic only
}
Enter fullscreen mode Exit fullscreen mode

2. Call the server from the client

// app/components/CheckoutButton.tsx (client component)
'use client';
import { useState } from 'react';

export default function CheckoutButton() {
  const [loading, setLoading] = useState(false);

  const startCheckout = async () => {
    setLoading(true);
    const res = await fetch('/api/stripe/checkout', { method: 'POST' });
    // handle response...
    setLoading(false);
  };

  return <button onClick={startCheckout} disabled={loading}>Buy</button>;
}
Enter fullscreen mode Exit fullscreen mode

3. Remove NEXT_PUBLIC_ prefixes for real secrets

If you have already renamed a secret, revert the name in the source and run a clean build:

- const stripeKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY;
+ const stripeKey = process.env.STRIPE_SECRET_KEY; // server‑only
Enter fullscreen mode Exit fullscreen mode

4. Verify with KeyDrift

Run a free KeyDrift scan (read‑only, no credentials) after the change:

npx keydrift scan --path ./out
Enter fullscreen mode Exit fullscreen mode

The scan should no longer report the secret in the client bundle.

Caveats

Caveat Details
Environment variable duplication If you need a value both on server and client (e.g., a public API key), store it separately as NEXT_PUBLIC_... and keep the secret version (..._SECRET) only on the server.
Third‑party libraries Some libraries (e.g., Stripe.js) expect a public key (pk_test_...). Ensure you are not accidentally passing a secret key to such libraries.
Build caching After renaming variables, clear .next or run next build --no-cache to avoid stale bundles that still contain the leaked value.
Server‑side rendering (SSR) vs. static generation In getStaticProps the code runs at build time on the server, so secrets are safe there. However, any data returned to the page becomes part of the HTML and can be inspected, so avoid embedding raw secrets in the returned props.
Dynamic imports Importing a module that reads a secret inside a client component will cause the same leak. Keep such imports confined to server‑only modules.

KeyDrift provides a concrete, read‑only audit that surfaces these leaks before they reach production. By moving secret usage back to server‑only code and avoiding the NEXT_PUBLIC_ prefix for real credentials, you eliminate the most common source of client‑bundle secret exposure in Next.js projects.

For more detailed guidance see the KeyDrift Fix Guides on “exposed keys by tool and credential.”

Top comments (0)