Prompt Details
Model
(claude-4-7-opus)
Token size
2,046
Example input
[CODEBASE_CONTEXT]: Next.js 14 App Router B2B SaaS for freelance invoicing. Solo founder 10 months into build, around 5000 dollars MRR. Stack is Stripe for billing Clerk for auth Supabase for database Resend for email. Deployed on Vercel with Axiom for logging.
[CODE_TO_AUDIT]: File app lib stripe ts contains import Stripe from stripe and exports const stripe equals new Stripe with hardcoded sk live 51ABCxyz key. File app api webhooks stripe route ts has export async function POST that reads body and runs console log Webhook received body then parses event and calls logger info Processing event with full event and user object. File dot env local committed to repo contains DATABASE URL postgresql with embedded password STRIPE SECRET KEY sk live and NEXT PUBLIC STRIPE SECRET KEY sk live. File app api auth login route ts returns error message stack and query in 500 response.
[SENSITIVE_ASSETS]: Stripe live secret key Supabase Postgres connection string with password customer emails freelancer tax IDs on Team tier invoice amounts client company names internal project data.
[DEPLOYMENT_SETUP]: Vercel production deployment. Environment variables set in Vercel dashboard. Axiom receives all application logs through axiomhq js package. No secret scanning configured in CI. GitHub repository is public.
[AUDIT_SCOPE]: thorough
Example output
### π¨ Exposure Summary
**3 Critical, 4 High, 3 Medium, 2 Low.** This codebase has catastrophic exposure β a live Stripe secret key and Supabase database credentials are almost certainly sitting in a public GitHub repository right now, and one of those keys is additionally shipped to every visitor's browser. Assume compromise and rotate every credential in the next hour before doing anything else.
---
### π― Top 3 Urgent Actions
1. **Rotate the Stripe live secret key immediately.** Go to https://dashboard.stripe.com/apikeys β "Roll key" on the Secret key. The key starting `sk_live_51ABCx...` must be considered compromised because (a) it is hardcoded in source, (b) it appears committed in `.env.local` in a **public** GitHub repo, and (c) it is also exposed as `NEXT_PUBLIC_STRIPE_SECRET_KEY`, meaning it has been bundled into your frontend JavaScript and served to every user who ever loaded the app. After rolling, update the new value in Vercel dashboard only β never in code.
2. **Rotate the Supabase database password and review access logs.** Go to Supabase dashboard β Project Settings β Database β Reset database password. The `DATABASE_URL` with embedded password is in `.env.local` committed to a public repo. Then check Supabase logs for any queries from IPs you don't recognize over the last 10 months. Consider this a potential breach of all customer data (emails, tax IDs, invoice amounts, client names) until proven otherwise.
3. **Purge secrets from git history and make the repo private.** Rotating isn't enough β the old keys remain valid in every git clone until you rotate at the provider (done in steps 1β2), AND you must scrub history so the next leak doesn't repeat. Run `git filter-repo --path .env.local --invert-paths` (or use BFG Repo-Cleaner), force-push, and switch the repo to private on GitHub. Then add `.env*` to `.gitignore` before committing anything else.
---
### π΄ Critical Findings
#### π΄ `app/lib/stripe.ts:~3`
**Finding:** Stripe live secret key hardcoded in source.
```ts
export const stripe = new Stripe("sk_live_51ABCxyz...") // redacted: sk_live_51ABCxβ¦[REDACTED]
```
**Why critical:** An attacker who finds this key β and they will, GitHub is crawled continuously by automated secret-scanners within minutes of a push β can create charges against your Stripe account, issue refunds to attacker-controlled cards (draining your balance), read all customer payment methods, view every invoice and customer email, and create new products/prices. Because it's a `sk_live_` key (not restricted), it has full account privileges. Stripe will email you when their own scanner catches it, but attackers usually find it first.
**Remediation:**
1. Rotate at https://dashboard.stripe.com/apikeys (do this first, before anything else).
2. Move the new key to Vercel env var `STRIPE_SECRET_KEY` (dashboard β Settings β Environment Variables, mark as Sensitive, Production scope only).
3. Replace the hardcoded value with `process.env.STRIPE_SECRET_KEY` and add a runtime check that throws on startup if missing.
4. Review Stripe's "Events" log for the last 10 months for any suspicious activity (unexpected refunds, new connected accounts, payment method reads).
---
#### π΄ `.env.local` (committed to public GitHub repo)
**Finding:** Production secrets committed to version control in a public repository.
```
DATABASE_URL=postgresql://postgres:[REDACTED]@db.[project].supabase.co:5432/postgres
STRIPE_SECRET_KEY=sk_live_51ABCxβ¦[REDACTED]
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_51ABCxβ¦[REDACTED]
```
**Why critical:** A public repo with `.env.local` means every historical commit that touched this file is a live credential store that anyone on the internet can `git clone` right now. Automated scanners (truffleHog, gitleaks, GitHub's own secret scanning, plus countless attacker-run crawlers) index public repos within minutes. The Supabase `DATABASE_URL` grants the `postgres` superuser role β full read/write/drop on every table including customer PII, tax IDs, invoice data, and client company names. This is potentially a full data breach trigger under GDPR (if any EU customers) with 72-hour notification requirements.
**Remediation:**
1. Rotate Supabase password (step 2 in Top Actions above).
2. Rotate Stripe key (step 1 in Top Actions above).
3. Add `.env*` (and explicitly `.env.local`, `.env.production`) to `.gitignore`.
4. Purge from git history using `git filter-repo` or BFG β **rotating alone is not enough**, the old credentials remain valid at the provider until you rotate there.
5. Make the repo private on GitHub: Settings β Danger Zone β Change visibility.
6. Review Supabase's auth logs, database logs, and connection logs for unknown IPs. Check Stripe's event log.
7. Determine if GDPR/CCPA breach notification is required based on your customer geography.
---
#### π΄ `.env.local` line containing `NEXT_PUBLIC_STRIPE_SECRET_KEY`
**Finding:** Stripe **secret** key prefixed with `NEXT_PUBLIC_`, bundling it into the browser.
```
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_51ABCxβ¦[REDACTED]
```
**Why critical:** In Next.js, any env var prefixed `NEXT_PUBLIC_` is inlined into the JavaScript bundle sent to every browser. This means your Stripe secret key is not just in a public repo β it's been delivered directly to every person who has ever visited your site. Open DevTools on your production app, Ctrl-F through any JS chunk, and the key is sitting there in plaintext. This is strictly worse than the repo leak because it required zero effort for attackers (anyone viewing your site has it).
**Whoever set this up likely confused the publishable key (`pk_live_...`, safe for frontend) with the secret key (`sk_live_...`, backend only).**
**Remediation:**
1. Rotate the Stripe secret key (Top Action #1).
2. Delete the `NEXT_PUBLIC_STRIPE_SECRET_KEY` var from Vercel and from `.env.local`.
3. If you need a Stripe key in the browser, use the **publishable** key only: `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...`. Publishable keys are designed to be public.
4. Audit all `NEXT_PUBLIC_*` env vars in Vercel β anything sensitive with that prefix is already leaked.
5. Rebuild and redeploy to remove the old key from the bundle. Old CDN-cached bundles may still contain it, so also invalidate Vercel's build cache.
---
### π High Findings
#### π `app/api/webhooks/stripe/route.ts` β logging full webhook event and user object
**Finding:**
```ts
console.log("Webhook received", body)
logger.info("Processing event", { event, user })
```
**Why high:** Stripe webhook events contain rich PII β customer emails, full names, billing addresses, last-4 of payment methods, sometimes tax IDs, and invoice line items that reveal your customers' client relationships and revenue. These logs flow through `console.log` β Vercel runtime logs AND through `axiomhq/js` β Axiom. You now have PII sitting in two third-party observability platforms, each with its own retention policy, access controls, and breach risk. If anyone on your Axiom account is compromised (or if Axiom itself is), every customer's billing data is exposed. Under GDPR this is a data processor relationship you likely haven't disclosed in your privacy policy.
Additionally, Stripe events can contain partial card data and webhook signing payloads that attackers could use to understand your integration.
**Remediation:** Log only non-PII identifiers. See patch in Remediation Patches section.
---
#### π `app/api/auth/login/route.ts` β error stack, message, and query in 500 response
**Finding:** 500 response body returns `{ error: err.message, stack: err.stack, query }` to the client.
**Why high:** This leaks (1) your database schema and query structure via ORM error messages ("column `users.stripe_customer_id` does not exist"), (2) internal file paths and line numbers via stack traces, (3) the user's submitted query β which on a **login** endpoint means the attacker-submitted email and potentially password attempts echoed back, and (4) library versions which help attackers target known CVEs. On a login endpoint specifically, returning the query is a credential-reflection risk: if a user accidentally types their password in the email field, it's now in browser history and potentially in your error tracking.
**Remediation:** Return generic error to client, log detailed error server-side with PII redacted. Patch below.
---
#### π No secret scanning in CI
**Why high:** You already leaked three credentials. Without a pre-commit hook or CI secret scanner, you will do it again. GitHub's native secret scanning only runs on push (too late) and only for supported patterns.
**Remediation:** Add `gitleaks` as a pre-commit hook and as a GitHub Action. Config in Hardening section.
---
#### π Repository is public with no clear indication it should be
**Why high:** A paid SaaS at $5k MRR has competitive risk from a public repo (competitors see your schema, pricing logic, feature roadmap from commit messages), legal risk (if you use any GPL/AGPL dependencies you're potentially obligated to publish β and if not, you may be violating licenses of dependencies you assumed were MIT), and the compounding leak risk we've already seen.
**Remediation:** Switch to private on GitHub: repo β Settings β General β Danger Zone β Change visibility β Make private. Unless there's a specific reason it's public (open source, marketing), default to private for SaaS code.
---
### π‘ Medium Findings
#### π‘ `logger.info` receives raw `user` object
Even with Stripe payload redacted, logging the full `user` object likely includes email, Clerk user ID, internal database ID, and possibly roles/metadata. Log `user.id` only, never the object. (Grouped with High finding above but rated Medium as a pattern to audit globally β search the codebase for every `logger.` and `console.` call and apply the same principle.)
#### π‘ No environment variable validation at startup
With env vars spread across Vercel dashboard and `.env.local`, a missing var fails at runtime in production (often mid-request) instead of at deploy time. Use `zod` or `t3-env` to validate all required env vars at app startup.
#### π‘ Clerk + Supabase without RLS verification
Not visible in provided code, but worth flagging: if Supabase tables aren't protected by Row-Level Security policies tied to Clerk user IDs, the `DATABASE_URL` leak is even worse β and even without a leak, any Supabase client-side call (anon key) could read across tenants. **I cannot verify this from the provided code; please confirm RLS is enabled on every table.**
---
### π΅ Low Findings
- **Axiom token exposure unclear.** The `axiomhq/js` package needs an API token. Ensure `AXIOM_TOKEN` is backend-only and not `NEXT_PUBLIC_` prefixed.
- **No rate limiting mentioned on `/api/auth/login`.** Clerk handles its own, but if this endpoint wraps Clerk, confirm you're not bypassing Clerk's brute-force protection.
---
### π‘οΈ Remediation Patches
**1. `app/lib/stripe.ts` β remove hardcoded key**
```ts
// BEFORE
import Stripe from "stripe"
export const stripe = new Stripe("sk_live_51ABCxyz...")
```
```ts
// AFTER
import Stripe from "stripe"
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY is not set")
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-06-20",
})
```
**2. `app/api/webhooks/stripe/route.ts` β safe webhook logging**
```ts
// BEFORE
export async function POST(req: Request) {
const body = await req.text()
console.log("Webhook received", body)
const event = stripe.webhooks.constructEvent(body, sig, secret)
logger.info("Processing event", { event, user })
// ...
}
```
```ts
// AFTER
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get("stripe-signature")
if (!sig) return new Response("Missing signature", { status: 400 })
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
logger.warn("webhook.signature_invalid", { eventIdAttempt: null })
return new Response("Invalid signature", { status: 400 })
}
// Log only non-PII identifiers
logger.info("webhook.received", {
eventId: event.id,
eventType: event.type,
livemode: event.livemode,
userId: user?.id, // internal ID only, never the user object
})
// ... handle event
return new Response("ok", { status: 200 })
}
```
**3. `.gitignore` β prevent future env leaks**
```
# BEFORE β .env.local was tracked
```
```gitignore
# AFTER β add to .gitignore
.env
.env.*
!.env.example
*.pem
*.key
```
Then: `git rm --cached .env.local` and commit, followed by history purge (see Top Action #3).
**4. `app/api/auth/login/route.ts` β safe error responses**
```ts
// BEFORE
catch (err) {
return Response.json(
{ error: err.message, stack: err.stack, query },
{ status: 500 }
)
}
```
```ts
// AFTER
catch (err) {
const errorId = crypto.randomUUID()
logger.error("auth.login.failed", {
errorId,
message: err instanceof Error ? err.message : "unknown",
// Never log `query` on an auth route β may contain credentials
})
return Response.json(
{ error: "Authentication failed", errorId },
{ status: 500 }
)
}
```
**5. `src/env.ts` β new file, env var validation at startup**
```ts
// AFTER β new file
import { z } from "zod"
const envSchema = z.object({
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
DATABASE_URL: z.string().url(),
AXIOM_TOKEN: z.string().min(1),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
// Explicitly reject the footgun:
NEXT_PUBLIC_STRIPE_SECRET_KEY: z.undefined({
message: "Secret keys must NEVER be NEXT_PUBLIC_ prefixed",
}).optional(),
})
export const env = envSchema.parse(process.env)
```
Import `env` from this module everywhere instead of `process.env` directly.
---
### π Hardening Recommendations
1. **`gitleaks` pre-commit hook + GitHub Action.** Install `pre-commit` and add a `.pre-commit-config.yaml` with the `gitleaks/gitleaks` hook. Add a GitHub Action that runs `gitleaks detect` on every push and blocks merges on findings. This would have caught all three Critical findings before they shipped.
2. **Log redaction middleware for Axiom.** Wrap your `axiomhq` logger with a redaction layer that strips `email`, `phone`, `taxId`, `address`, `stripe_customer_id`, `password`, `token`, and any field matching `/secret|key|token|password/i` before the log leaves your process. Libraries: `pino`'s redact option works well here if you switch from `axiomhq/js` directly to `pino` with Axiom transport.
3. **Vercel environment variable scoping.** In Vercel dashboard, set `STRIPE_SECRET_KEY` and `DATABASE_URL` as "Sensitive" (encrypted at rest, not viewable after save) and scoped to Production only. Use a separate Stripe test key for Preview and Development environments β this prevents a preview deployment bug from charging real cards.
4. **Stripe Restricted Keys instead of the full secret.** Replace `sk_live_...` with a Stripe Restricted Key scoped to only the resources you actually use (e.g., write on Charges/Customers/Invoices, read-only on Products). If it leaks again, blast radius is dramatically smaller. Create at https://dashboard.stripe.com/apikeys β Create restricted key.
5. **Supabase RLS audit + connection pooling.** Enable Row-Level Security on every table with policies tied to `auth.uid()` (or your Clerk JWT claim). Use Supabase's connection pooler URL (`pgbouncer` on port 6543) instead of direct Postgres, and rotate the pooler password separately. This means even if `DATABASE_URL` leaks again, an attacker hits RLS before they can read cross-tenant data.
---
### β
Remediation Checklist
- [ ] Rotate Stripe live secret key at https://dashboard.stripe.com/apikeys
- [ ] Rotate Supabase database password in Supabase dashboard
- [ ] Review Stripe event log and Supabase connection log for unauthorized access (last 10 months)
- [ ] Make GitHub repository private
- [ ] Delete `NEXT_PUBLIC_STRIPE_SECRET_KEY` from Vercel env vars and `.env.local`
- [ ] Add `.env*` to `.gitignore` and run `git rm --cached .env.local`
- [ ] Purge `.env.local` from git history with `git filter-repo` or BFG, then force-push
- [ ] Apply patches to `app/lib/stripe.ts`, webhook route, and login route
- [ ] Add env var validation module (`src/env.ts` with Zod) and import at app entry
- [ ] Install `gitleaks` pre-commit hook and GitHub Action
- [ ] Replace full Stripe secret with a Stripe Restricted Key scoped to actual usage
- [ ] Confirm Supabase RLS is enabled on every table with customer data
---
### π‘ Founder's Summary
This is an emergency, not a cleanup β a live Stripe key and database password are in a public GitHub repo, and the Stripe key is also being shipped to every visitor's browser, meaning both are effectively public knowledge right now. Stop everything and do the three Top Actions above in order: rotate Stripe, rotate Supabase, purge git history and make the repo private β all in the next hour. Once credentials are rotated the immediate bleeding stops, and then you can work through the logging fixes and hardening over the rest of the week; but understand that until you rotate, attackers with automated scanners may already have your keys.
---
**A note on scope:** I audited the specific code snippets and context you provided. I could not see your full `.gitignore`, the rest of your API routes, your Clerk integration details, or your Supabase RLS policies. To do a fully thorough audit I'd want to see: the full repo file tree, all API route handlers, your `middleware.ts`, Supabase SQL migrations (for RLS verification), and your Vercel env var list (names only, not values). The findings above are what's visible β there may be more.
By purchasing this prompt, you agree to our terms of service
CLAUDE-4-7-OPUS
A senior-security-engineer style codebase auditor for solo SaaS
founders worried about accidental leaks.
Designed to surface hardcoded credentials, PII in logs, secrets
in client bundles, verbose error responses, and repo hygiene
issues.
You get a severity-ranked findings report, redacted leak
previews, ready-to-apply fix patches, and a prioritized
remediation checklist.
...more
Added 2 weeks ago
