Why You Should Never Trust the Frontend for Server-Side Verification
Earlier today, I was booking a train ticket online. The platform required NIN (National Identification Number) verification before letting me continue. I entered a valid NIN. The verification call failed with a 500 and a
Earlier today, I was booking a train ticket online. The platform required NIN (National Identification Number) verification before letting me continue. I entered a valid NIN. The verification call failed with a 500 and a generic SOMETHING_WENT_WRONG. Not an invalid-NIN error — an actual server failure. Tried again. Same result.
Curious, I opened Chrome DevTools to see what was actually happening on the wire.
What I found
The flow looked roughly like this:
Frontend --> POST /verify-nin-details?ninNumber=xxxx
Server --> { status, message, result, errorMessages }
Frontend --> reads response, decides locally whether to proceed
That third step is the whole problem. The server returns a JSON blob describing what happened, and the frontend reads it and makes the call — entirely on its own — about whether verification succeeded. There's no session flag set server-side. No token issued on success. The "verified" state exists only as a JSON object sitting in the browser for as long as the page is open.
Once you can see the shape of a successful response — trivial, since it's sitting right there in the app's own JS — Chrome's Local Overrides feature (Network tab → right-click a request → Override content, intended for testing how your frontend handles different responses) will happily serve a response matching that shape instead of the real one. The frontend has no way to tell the difference. It just believes whatever JSON it receives.
Nothing was actually verified server-side. My browser told my own frontend code a story, and the frontend believed it.
This isn't a "hack"
Chrome is doing exactly what it's built to do — Local Overrides exists so you can test your own frontend's error handling without hammering a real backend. The actual vulnerability is architectural: whenever a security-relevant decision (identity verified, payment confirmed, KYC passed) is made by client-side JS reading a response, rather than by the server maintaining and re-checking that state itself, the decision is only as trustworthy as the user's browser. Which is to say, not trustworthy at all.
Anyone who opens DevTools, runs a proxy like mitmproxy or Charles, or writes a two-line fetch monkeypatch can do this:
const origFetch = window.fetch;
window.fetch = async (...args) => {
const res = await origFetch(...args);
if (String(args[0]).includes('verify-nin-details')) {
return new Response(JSON.stringify({
status: 200,
result: { verified: true },
errorMessages: []
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
return res;
};
No special tooling required, no account access needed — just a browser.
The actual fix
- Verification state belongs server-side. On success, the server should set a session flag or issue a short-lived signed token — something it can independently validate later, not something the client reports back to it.
- Re-check before anything irreversible. Before confirming a booking, issuing a ticket, or unlocking an account, the server should check its own record of the verification, not trust that the frontend reached a particular UI state.
- Client-side response handling should only drive UX. What the user sees next — success screen, retry button, error message — can live in the frontend. Whether an action is authorized can never live there.
This pattern shows up constantly: age gates, KYC flows, payment "confirmed" states, feature unlocks behind a paywall — any place where "the frontend got a nice response" is quietly being treated as proof that something happened.
What I didn't do
I'm deliberately not including the platform name, the exact endpoint, or the full response payload here — the point isn't "here's how to get through someone's identity check," it's the trust-boundary lesson. I reported the underlying bug (valid NIN → 500) to the platform directly, since that's worth fixing on its own merits regardless of the architecture question.
The takeaway
If your server returns data describing what happened, and your frontend is the one deciding what that data means for authorization — you don't have server-side verification. You have a browser-side opinion that the server happened to suggest once.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.