Secret API keys are not exposed to the client-side bundle
Why it matters
A Stripe secret key (sk_live_*) exposed in a client-side bundle or a NEXT_PUBLIC_ environment variable is readable by every visitor who opens DevTools. An attacker can use it to create refunds, list customers, retrieve stored card fingerprints, or cancel subscriptions without touching your server. PCI-DSS 4.0 Req 3.4 prohibits storing or transmitting secret key material outside controlled server environments. OWASP A02 (Cryptographic Failures) flags this as one of the highest-severity credential exposures because the key grants API-level access to your entire Stripe account — charges, payouts, and customer data — to any actor who extracts it from your bundle.
Severity rationale
Critical because a leaked secret key grants full Stripe account API access to anyone who reads the client bundle, enabling unauthorized refunds, payouts, and customer data exfiltration.
Remediation
Move the secret key out of any client-side file and any NEXT_PUBLIC_ environment variable. The publishable key (pk_*) is the only Stripe credential that belongs in browser code.
// lib/stripe.server.ts — server-only module
import Stripe from 'stripe'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
})
// Client component — only the publishable key
import { loadStripe } from '@stripe/stripe-js'
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)
Verify .env.example uses STRIPE_SECRET_KEY (no NEXT_PUBLIC_ prefix). If the key was ever committed or bundled, rotate it immediately in the Stripe Dashboard.
Detection
-
ID:
no-secret-keys-client -
Severity:
critical -
What to look for: Count every client-side file (components, pages, layouts, client-entry modules) and enumerate each one for payment provider secret key patterns. Look for string patterns matching
sk_live_,sk_test_,rk_live_,whsec_in at least 5 search locations: components directory, pages directory, public-prefix env vars, bundler define blocks, and test fixtures. Check forNEXT_PUBLIC_STRIPE_SECRET_KEYor similar public-prefix environment variable names applied to secret credentials. Examinevite.config.ts,next.config.ts, or webpack config fordefineblocks that could inject secret values into the bundle. Also check for secret keys in git history hints (.envcommitted accidentally) or in test fixtures shipped with the client. -
Pass criteria: No payment provider secret keys appear in client-side code, client-side environment variables, or client-bundled config. All secret credentials are strictly referenced in server-side files only (API routes, server actions, edge functions, backend modules). No more than 0 secret key patterns found across all client-side files. Report even on pass: "Searched X client-side files across Y directories — 0 secret key patterns found."
-
Fail criteria: Any secret key is found in a client-side component file, a page component, a
NEXT_PUBLIC_variable, or a bundlerdefineblock that runs in the browser. -
Do NOT pass when: Secret keys appear only in comments or dead code — commented-out secrets are still exposed in the client bundle and can be extracted. A secret in a comment is NOT a pass.
-
Skip (N/A) when: The project has no payment provider integration detected (no payment SDK dependency found in
package.jsonand no payment-related files found). -
Detail on fail: Name the secret type and its exact location. Example:
"Stripe secret key (sk_test_*) referenced via NEXT_PUBLIC_STRIPE_SECRET_KEY in components/CheckoutForm.tsx — exposed to all clients" -
Cross-reference: The Environment Security audit covers comprehensive secret scanning across all file types, not just payment-related code.
-
Remediation: Secret keys must never leave the server. Move all references to server-side code only (API routes, server actions). Use the publishable key (
pk_*) on the client and the secret key (sk_*) only on the server:// WRONG — in a client component or NEXT_PUBLIC_ env var const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY!) // CORRECT — in a server-only file (API route, server action) // app/api/payment/route.ts import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '...' })On the client, only the publishable key is needed:
// Client component import { loadStripe } from '@stripe/stripe-js' const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)Audit your
.env.exampleto ensureSTRIPE_SECRET_KEY(withoutNEXT_PUBLIC_) is used for the secret.
External references
- cwe · CWE-312 — Cleartext Storage of Sensitive Information
- cwe · CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
- owasp:2021 · A02
- pci-dss:4.0 · Req 3.4 — PAN protected wherever stored
Taxons
History
- 2026-04-18·v1.0.0·Initial import from ecommerce-payment-security·automated