DEV Community

A client's vibe-coded app was leaking data. Here's how I closed it in 24 hours

Fahad Azhar on November 07, 2026

A client's internal app (an inventory dashboard connected to 4 live Shopify stores) had been built fast, the way a lot of tools get built now. Then...
Collapse
 
rushdeploy profile image
Rishabh Mishra •

Treating every token as stolen despite clean 24-hour logs is the right call — logs that short can't prove a negative.
The part I'd steal for my own projects is the exceptions list with written reasons; without it, a daily scan just trains people to ignore the alerts it raises.

Collapse
 
fahadazhar1 profile image
Fahad Azhar •

Exactly. 24 hours of clean logs only tells you nobody used the tokens in those 24 hours. And yes, without the exceptions list the report was getting noisy fast: a few findings are intended, like a public chat widget that writes anonymous sessions, and seeing those every morning would have trained me to skim. Writing the reason down also forces the question "is this really intended?" once.

Collapse
 
rushdeploy profile image
Rishabh Mishra •

That chat widget example is a good one — an intended finding that keeps surfacing is exactly how a scan loses trust.
One thing I'd add is a review date on each exception, so an 'intended for now' decision gets revisited instead of living forever.
If the reason ever stops matching what the widget actually does, that date is what catches it.

Thread Thread
 
fahadazhar1 profile image
Fahad Azhar •

Ha, that's the gap in my setup right now. Every exception has a reason, but nothing makes me look at it again, so "intended for now" quietly turns into "intended forever". Your last line is the real point: the reason can be true the day I write it and wrong six months later. A review date on each entry is next on my list. Thanks, this thread has made the scanner better.

Thread Thread
 
rushdeploy profile image
Rishabh Mishra •

A light way to do it is a 15-minute recurring calendar block each week to re-read the exceptions list. Keep one line per exception — what was excepted and why — so anything past its review date expires instead of accumulating silently.

Thread Thread
 
fahadazhar1 profile image
Fahad Azhar •

Thanks, I actually added this after your first comment. Every exception now has an owner and a review-by date. If one has no date, it's flagged in the daily security report, and once the date passes it comes back as a high-priority finding until someone re-checks it. So nothing stays approved forever without anyone looking.

The calendar block is a good idea on top of that, because the report only catches what's already on the list.

Collapse
 
alexcodebytes profile image
Oleksandr •

The using (true) demo policy is the silent killer here. The rotation part is what most guides miss—re-running OAuth doesn't kill old tokens, so rotating the client secret is mandatory. Great reminder that AI makes building fast but doesn't do the security review.

Collapse
 
fahadazhar1 profile image
Fahad Azhar •

Thanks. The OAuth part surprised me too: you re-run it, get a fresh token, and assume the old one is dead. It isn't until you rotate the secret and revoke the old one. That's why the last step is testing with the old token on purpose and expecting a 401.

Collapse
 
alexcodebytes profile image
Oleksandr •

Testing with the old token to ensure a 401 is the only way to truly prove it's dead. Assuming a token expired just because you re-ran the OAuth flow is exactly how these leaks slip through the cracks.

Thread Thread
 
fahadazhar1 profile image
Fahad Azhar •

Exactly. The worrying part is that nothing tells you. The old token keeps working without a sound, so if you never test it, you'll never know. Now that 401 check is a fixed step on my list whenever I rotate anything.

Thread Thread
 
alexcodebytes profile image
Oleksandr •

"The old token keeps working without a sound" is the whole problem in one sentence. A successfully revoked token and a forgotten one look identical from the outside — both return 200, and nobody notices the difference until something goes wrong.

What helped on my side was adding the rotation date to the secret's metadata, so a stale credential is visible at a glance instead of living only in someone's memory. Combined with the 401 check as a fixed step, it's the closest thing to a real guarantee. Otherwise you're just trusting that the last person who rotated it was thorough.

Collapse
 
alexcodebytes profile image
Oleksandr •

Testing the old token to verify the 401 is the only real proof it's dead. Assuming re-running OAuth kills the old credentials is a classic, dangerous trap.

Collapse
 
alexshev profile image
Alex Shev •

The attacker-side verification is the crucial closing step: “we deployed a fix” is not the same as proving old credentials fail and the intended path still works. The daily scanner is also better as a report than an auto-remediator; an explicit allowlist with an owner and expiry turns exceptions into reviewable risk instead of permanent prototype residue.

Collapse
 
fahadazhar1 profile image
Fahad Azhar •

Agreed on both. The expiry point is a fair hit. My allowlist has a written reason for every exception, but no owner and no expiry date, so an exception from last month looks as valid as one from today. I'm adding a review-by date to each entry, and the scan will flag any that have expired. Thanks, that's a better design than mine.

Collapse
 
lastmileops profile image
lastmileops •

If some staff should only see a subset of the four stores, I'd add a staging test with one fake order in store A and one in B, and a staff login allowed to use only A. Check that the login can read A's order through the app API and gets no order data when it asks for B's by ID. If staff can edit orders, check that an edit to A works and that an attempted edit to B leaves its row unchanged. That covers the case where a caller passes the staff check but should still be denied access to a store.