Security Guides

Your Next.js App Is Leaking API Keys — Here's How Cursor AI Causes It

SecuriSky TeamApril 9, 20266 min read

The Client/Server Boundary Problem

Next.js has a critical security boundary: code in app/ components runs on the server by default,

but code marked "use client" runs in the browser.

The problem: AI coding tools don't always respect this boundary.

When you ask Cursor or GitHub Copilot to "add Stripe payment processing" or "connect to OpenAI",

the generated code often places server-side secrets in components that eventually get bundled for the client.

How It Happens

Pattern 1: Environment Variables in Client Components

// pages/checkout.tsx — marked "use client"

"use client";

// ⚠️ NEXT_PUBLIC_ prefix exposes this in the browser

// But even WITHOUT the prefix, if this file is imported

// by a client component, it gets bundled

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

Next.js strips non-NEXT_PUBLIC_ variables at build time — but only if the file never gets

imported by a client-side component tree. AI tools create complex import chains that are

hard to audit manually.

Pattern 2: Utility Functions Shared Across the Boundary

// lib/ai.ts — looks server-side

import OpenAI from 'openai';

export const openai = new OpenAI({

apiKey: process.env.OPENAI_API_KEY,

});

// app/components/chat.tsx — "use client" imports lib/ai.ts

"use client";

import { openai } from '@/lib/ai'; // ← pulls OPENAI_API_KEY into browser bundle

Pattern 3: Server Actions Called Incorrectly

// ⚠️ 'use server' directive is on the wrong component

"use server"; // This should be a top-level file directive, not inline

async function processPayment(amount: number) {

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // ...

}

What Gets Exposed

In production builds, these leaks expose:

  • Stripe secret keys — allows arbitrary charges and refunds
  • OpenAI API keys — unlimited spend on your account
  • Database connection strings — direct DB access
  • Supabase service role keys — bypasses all RLS
  • Twilio/SMS credentials — send messages on your behalf
  • How to Detect This

    Run curl https://yourapp.com/_next/static/chunks/*.js | grep -E "(sk_live|sk_secret|eyJ|postgres://)" to quick-check your bundles.

    Or run a SecuriSky scan — our API Key Leakage in JS module performs deep bundle analysis to find embedded secrets, even when obfuscated.

    The Fix

    1. Use Server Actions Properly

    // app/actions/stripe.ts
    

    "use server";

    export async function createPaymentIntent(amount: number) {

    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); return stripe.paymentIntents.create({ amount, currency: 'usd' });

    }

    2. Never Import Server Utilities in Client Components

    Create a clear separation:

  • lib/server/ — server-only utilities (never imported in "use client" files)
  • lib/client/ — client-safe utilities (no secrets)
  • 3. Use Next.js Server-Only Package

    // lib/stripe.ts
    

    import 'server-only'; // Throws at build time if imported by client code

    import Stripe from 'stripe';

    export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

    4. Audit Your Build Output

    npm run build
    

    Then check .next/static/chunks/ for any secret-like strings

    grep -r "sk_live" .next/static/

    Quick Fix Checklist

  • [ ] Install server-only package and add to all files with secrets
  • [ ] Move all process.env.SECRET_* references to app/api/ routes or Server Actions
  • [ ] Audit all "use client" component import trees
  • [ ] Run SecuriSky's API key leakage scanner on your deployed URL
  • Next.js API Key Exposure: How Cursor AI Leaks Secrets (2025) — SecuriSky Blog