
Case Study: How AI Helped Uncover a Critical Business Logic Flaw
What the AI actually found
The useful part of this case was not that the AI found some novel chain. It flagged a business rule humans had seen, but not pushed on hard enough.
The flow looked normal: a user started a purchase path, chose a plan, and the frontend eventually showed success. The AI noticed the client was sending a field that looked like a final authority marker, while the backend treated it as trustworthy. That kind of bug is easy to overlook because honest users never trigger the bad path.
Impact-wise, it was serious: a lower-privilege user could end up with access that should have been blocked by payment or approval.
Why this bug class is easy to miss
Business logic bugs usually do not crash. They show up as valid requests with the wrong meaning.
If you are reviewing a React app, a Next.js API route, or a monolith with a JSON API, it is tempting to focus on the visible flow:
- the button stays disabled until checkout finishes
- the UI redirects after a “success” response
- the account page reflects the upgraded state
That feels complete. It is not.
The real question is whether the server verifies the state transition on its own. If the client can submit status: "paid" or plan: "enterprise" and the backend accepts it, the UI was just decoration.
Reconstructing the workflow
The user path and the hidden assumption
The path looked harmless:
- Start with a free account.
- Open the upgrade or onboarding flow.
- Submit the final form.
- See the upgraded state in the UI.
The hidden assumption was that the frontend field represented a verified outcome, not just a user-controlled hint. That is where these bugs live. Someone names a field completed, approved, verified, or tier, and a later developer forgets that the name is not proof.
Where the server trusted the wrong field
A common failure pattern looks like this:
- the browser submits a form payload
- the API route copies fields into a database row
- a later request checks the stored row instead of the original source of truth
That breaks the trust boundary. The backend should accept only the minimum client input needed to identify the action, then derive the real entitlement from its own checks.
Here is the kind of mistake I would look for:
// unsafe pattern
app.post("/api/upgrade", async (req, res) => {
const { userId, plan, paid } = req.body;
if (paid) {
await db.users.update({
where: { id: userId },
data: { plan, status: "active" },
});
}
res.json({ ok: true });
});
The bug is not the syntax. The bug is the trust model.
Testing the flaw safely
Building a minimal repro in JavaScript
I like to reduce the issue to one request and one assertion. You do not need a full browser automation stack if the API already proves the problem.
const fetch = global.fetch;
async function testUpgrade(baseUrl, cookie) {
const response = await fetch(`${baseUrl}/api/upgrade`, {
method: "POST",
headers: {
"content-type": "application/json",
cookie,
},
body: JSON.stringify({
plan: "pro",
paid: true,
}),
});
return {
status: response.status,
body: await response.json().catch(() => null),
};
}
Then compare the server state before and after the request. The key test is whether the account changes without any independent payment confirmation.
Verifying impact without harming real users
Keep the test controlled:
- use a non-production account
- avoid real payment attempts
- do not modify other users' records
- record only the minimum proof needed
A good proof is often just “a free account gained access to a gated feature after a crafted request.” That is enough to establish impact. You do not need to consume a real service or touch data outside the test account.
Do not rely on the frontend screenshot as proof. The server state is what matters, and that is what the report should show.
What the AI added and what it did not
The AI was useful in two ways:
- it surfaced a suspicious field or transition that deserved review
- it helped rank the bug as a logic issue, not a cosmetic UI quirk
It did not replace the audit. A model can point to “the server trusts a client-controlled value,” but it still takes a human to confirm:
- what the business rule is
- which request actually crosses the trust boundary
- whether the backend rechecks authorization at the right point
That distinction matters. AI can narrow the search. It cannot prove intent, ownership, or entitlement by itself.
Fixing the logic on the backend
The fix belongs on the server, not in the button state.
Good fixes usually look like this:
- ignore client-supplied entitlement fields
- verify payment or approval through a trusted source
- store only server-derived status
- enforce authorization again on every protected API route
A safer pattern:
app.post("/api/upgrade", async (req, res) => {
const userId = req.auth.sub;
const payment = await paymentProvider.verifyRecentPayment(userId);
if (!payment?.valid) {
return res.status(403).json({ error: "payment required" });
}
await db.users.update({
where: { id: userId },
data: { plan: "pro", status: "active" },
});
res.json({ ok: true });
});
That removes the client's ability to assert its own privilege.
Signals to look for in future reviews
When I review an app for this class of bug, I look for these signals:
| Signal | Why it matters |
|---|---|
Client sends status, role, plan, or approved | These fields often carry trust they should not have |
Backend uses req.body as source of truth | The request body is user-controlled |
| UI state and server state are loosely coupled | The frontend can lie by accident or design |
| One request unlocks multiple actions | Small logic mistakes turn into bigger impact |
| No server-side revalidation on protected routes | Authorization becomes a one-time check |
If you see two or more of those in the same flow, slow down and inspect the trust boundary.
Conclusion
This case was a good reminder that AI works best as a second set of eyes, not as an authority. It helped point at the weak spot, but the real work was confirming that the backend trusted a client-controlled field and that the trust led to privilege it should never have granted.
That is the pattern to remember: the bug is usually not in the button. It is in the server accepting a story the client was never supposed to tell.


