TLDR: The PII leak that almost shipped wasn't from an attacker. It was from me. A
console.logI left in while debugging.
The Setup
I'm building a practice-management system — scheduling, billing, document extraction, a chat interface for the staff who use it every day.
Before shipping a big security pass last April, I sat down to audit the thing properly.
I had a checklist. Real scary stuff: SQL injection via the search filter, a chat endpoint that would accept a 50 MB body and happily amplify your AI costs, raw PostgREST filter params getting passed through unsanitized.
I found all of those. Fixed them. Felt good about it.
The Thing I Almost Missed
Here's what I was not looking for: the debug log I put in myself two weeks earlier.
I'd been chasing a flaky integration with Google Calendar and Xero. The API responses were confusing — I couldn't tell what was coming back — so I did what every developer does.
I logged the whole thing.
console.log("API response:", response.body)
Shipped it. Forgot about it.
That response body? Full of personal data. Names, appointment times, billing references. Sitting in Vercel logs, forever, queryable by anyone with access to the dashboard.
And then there was the error handling — the chat route was surfacing raw upstream API error bodies directly to the UI. Which means a failed Google or Xero call was printing whatever the API returned… straight to the browser.
Two PII exits I built myself. Neither of them was on my attacker checklist.
What I Fixed
Four commits, that night:
security: remove PII-leaking debug logs from chat route— stripped everyconsole.logthat touched an upstream response bodysecurity: stop leaking Google/Xero API response bodies to UI errors— error handling now surfaces a digest code, not the raw messagesecurity: sanitize search terms to prevent PostgREST filter injectionsecurity: cap chat request body at 256 KiB to block cost-amplification DoS
The last two are the ones that look impressive. The first two are the ones that mattered.
The Standard I Keep Coming Back To
On another project — a law firm system with strict SSN handling — the rule is written out plainly: SSN is never logged. Not "logged securely," not "redacted before export." Never in the log pipeline at all. The audit log detail payload explicitly excludes it.
That constraint is obvious for an SSN. It's less obvious for a Xero API response. But the principle is identical.
Logs go places. Vercel log drain. Datadog. A spreadsheet someone pulls for a client review. A support ticket where you paste context to debug something else six months from now.
Once PII is in the log stream, you've lost control of it.
Why This Matters To Me
I'm good at finding the attack vectors. I can audit for injection, for DoS, for open RLS policies — I enjoy that work.
But the scarier leaks are the ones I leave behind while debugging at 11pm. The console.log that felt so temporary I didn't even register writing it.
Before you ship: grep your own logs. Not for what an attacker put there. For what you put there.
P.S. The grep I now run before every security pass:
grep -rn "console.log\|response\.body\|req\.body" src/— looking for anything that touches a payload in a log call. Takes 30 seconds. Has saved me twice.