{"slug": "your-next-public-secret-is-already-in-the-browser-bundle", "title": "Your NEXT_PUBLIC secret is already in the browser bundle", "summary": "A developer has highlighted a critical security flaw in Next.js applications where developers inadvertently expose server-side secrets by prefixing environment variables with NEXT_PUBLIC_. The tool KeyDrift performs a read-only scan of client bundles to detect such leaks, cross-referencing credentials with the tool that introduced them. The developer recommends moving secret usage to server-side code and running a clean build to mitigate the risk.", "body_md": "In a Next.js app it’s easy to slip a secret (e.g., `STRIPE_SECRET_KEY`\n\n, OpenAI API key, Supabase JWT) into a client‑side bundle. When a server component reads `process.env.STRIPE_SECRET_KEY`\n\nit 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_`\n\nprefix—makes the value part of the JavaScript that every visitor downloads, turning a server‑only secret into a public leak.\n\n**Next.js environment variable scoping**\n\n`NEXT_PUBLIC_`\n\nprefix are stripped from the client bundle at build time. They are only available in server‑side code (`pages/api/*`\n\n, server components, `getServerSideProps`\n\n, etc.).\n`NEXT_PUBLIC_`\n\nprefix are injected into the client bundle and can be read from `process.env`\n\nin any browser‑executed code.**Accidental exposure**\n\n`const stripeKey = process.env.STRIPE_SECRET_KEY;`\n\nfrom a server component to a client component (or a shared utility imported by both).\n`STRIPE_SECRET_KEY`\n\nis undefined on the client.\n`NEXT_PUBLIC_STRIPE_SECRET_KEY`\n\n.\n**Why the leak is critical**\n\nKeyDrift 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).\n\nTypical output for a Next.js leak looks like:\n\n```\n[Critical] STRIPE_SECRET_KEY found in client bundle (tool: Next.js)\nLocation: static/chunks/pages/_app.js:1234\nRecommendation: Move usage to a server component or API route.\n```\n\nKeyDrift also flags variables that have been renamed with the `NEXT_PUBLIC_`\n\nprefix and marks them as **high** or **critical** depending on the credential type (e.g., Stripe secret key → critical).\n\n``` python\n// app/api/stripe/checkout/route.ts (server‑only)\nimport Stripe from 'stripe';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: '2023-10-16',\n});\n\nexport async function POST(req: Request) {\n  // server‑side logic only\n}\njs\n// app/components/CheckoutButton.tsx (client component)\n'use client';\nimport { useState } from 'react';\n\nexport default function CheckoutButton() {\n  const [loading, setLoading] = useState(false);\n\n  const startCheckout = async () => {\n    setLoading(true);\n    const res = await fetch('/api/stripe/checkout', { method: 'POST' });\n    // handle response...\n    setLoading(false);\n  };\n\n  return <button onClick={startCheckout} disabled={loading}>Buy</button>;\n}\n```\n\n`NEXT_PUBLIC_`\n\nprefixes for real secrets\nIf you have already renamed a secret, revert the name in the source and run a clean build:\n\n``` js\n- const stripeKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY;\n+ const stripeKey = process.env.STRIPE_SECRET_KEY; // server‑only\n```\n\nRun a free KeyDrift scan (read‑only, no credentials) after the change:\n\n```\nnpx keydrift scan --path ./out\n```\n\nThe scan should no longer report the secret in the client bundle.\n\n| Caveat | Details |\n|---|---|\nEnvironment variable duplication |\nIf 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. |\nThird‑party libraries |\nSome libraries (e.g., Stripe.js) expect a public key (`pk_test_...` ). Ensure you are not accidentally passing a secret key to such libraries. |\nBuild caching |\nAfter renaming variables, clear `.next` or run `next build --no-cache` to avoid stale bundles that still contain the leaked value. |\nServer‑side rendering (SSR) vs. static generation |\nIn `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. |\nDynamic imports |\nImporting a module that reads a secret inside a client component will cause the same leak. Keep such imports confined to server‑only modules. |\n\n**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_`\n\nprefix for real credentials, you eliminate the most common source of client‑bundle secret exposure in Next.js projects.\n\n*For more detailed guidance see the KeyDrift Fix Guides on “exposed keys by tool and credential.”*", "url": "https://wpnews.pro/news/your-next-public-secret-is-already-in-the-browser-bundle", "canonical_source": "https://dev.to/veristria/your-nextpublic-secret-is-already-in-the-browser-bundle-2ige", "published_at": "2026-08-28 20:59:58+00:00", "updated_at": "2026-08-28 21:18:21.881596+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Next.js", "KeyDrift", "Stripe", "OpenAI", "Supabase", "Replit", "Cursor"], "alternates": {"html": "https://wpnews.pro/news/your-next-public-secret-is-already-in-the-browser-bundle", "markdown": "https://wpnews.pro/news/your-next-public-secret-is-already-in-the-browser-bundle.md", "text": "https://wpnews.pro/news/your-next-public-secret-is-already-in-the-browser-bundle.txt", "jsonld": "https://wpnews.pro/news/your-next-public-secret-is-already-in-the-browser-bundle.jsonld"}}