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.
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.
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.
ID: ecommerce-payment-security.payment-integration.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 for NEXT_PUBLIC_STRIPE_SECRET_KEY or similar public-prefix environment variable names applied to secret credentials. Examine vite.config.ts, next.config.ts, or webpack config for define blocks that could inject secret values into the bundle. Also check for secret keys in git history hints (.env committed 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 bundler define block 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.json and 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.example to ensure STRIPE_SECRET_KEY (without NEXT_PUBLIC_) is used for the secret.