CriticalAuth Bypass· 4 min read

The Boolean That Owned Every Account

A passwordless login trusted the browser to grade its own homework — so I flipped one false to true and walked into anyone's account with just their email.

Target anonymized · a retail super-app

The target was a retail super-app — one of those sprawling products that crams grocery delivery, payments, loyalty points, and, lately, an in-app AI shopping assistant behind a single login. I was working its staging environment. The first thing I noticed at the front door was that there was no door: no password. You type your email, a six-digit code lands in your inbox, you type it back, you're in. Passwordless flows are lovely for users and often a soft spot for defenders, because all the security lives in one fragile handshake. I wanted to watch that handshake very, very closely.

The spark

I did the obvious thing first: I entered a victim email I controlled, then deliberately typed a wrong code — 111111 — and hit submit. I wasn't trying to guess it. I was trying to see how the app reacts to failure.

In Burp, the browser fired a POST to /api/auth/otp/validate, and the server answered with something almost insultingly simple:

{"success":false}

That tiny body made me pause. Whenever a server hands the frontend a naked boolean and lets the frontend decide what happens next, I start asking the same question: who is actually in charge here — the server, or the browser holding the server's answer?

Digging in

My first hypothesis was boring and wrong. I figured I'd brute-force the OTP: six digits is only a million tries. Dead end. The endpoint rate-limited hard and locked the flow after a handful of misses. Good hygiene there.

So I refined the question. Instead of guessing the code, what if the server never really checked whether I'd solved it, and just trusted the client's belief that I had? The {"success":false} response was the client asking "did this pass?" — and the server was answering honestly. But honesty isn't authority. If the frontend was the thing gating the next step, then the response was mine to rewrite in flight.

I flipped it. In Burp I intercepted the response (not the request) to the validate call and changed one word:

{"success":true}

Then I watched the network tab and nearly missed the important part. On seeing success:true, the frontend didn't just paint a green checkmark — it immediately fired a second, background request to /api/auth/customer-lookup, carrying nothing but the victim's email. That automatic follow-up was the tell.

I almost talked myself out of it here — my second dead-end worry. Maybe customer-lookup just returns an anonymous, unauthenticated blob and the real session gets minted somewhere protected. So I read the response headers instead of the body. And there it was.

The exploit

The server processed the lookup as if the OTP had genuinely been verified, and issued live session cookies:

POST /api/auth/otp/validate HTTP/2
Host: app.example.com
Content-Type: application/json

{"email":"victim@example.com","otp":"111111"}

Server says {"success":false} → I intercept the response, change it to {"success":true}, forward it, and the browser auto-sends:

POST /api/auth/customer-lookup HTTP/2
Host: app.example.com
Content-Type: application/json

{"email":"victim@example.com"}
HTTP/2 200 OK
Set-Cookie: TokenId=<valid-session-token>; Secure; HttpOnly
Set-Cookie: session_state=<tracking>; Secure

My browser now held a valid TokenId. One page refresh and the UI dutifully hydrated the victim's account — order history, saved addresses, and their private conversations with the in-app AI assistant. I never knew a single digit of the real code.

The entire vulnerability lives in one sentence: the client asked "did the OTP pass?" and the server answered by trusting the client's own claim. Authentication state must live on the server and gate every privileged step — the browser is a hostile narrator.

Impact

Zero-click account takeover, universal, unauthenticated. The only thing an attacker needs is a target's email address — no code, no phishing, no malware. Because the session cookies are genuine, everything downstream treats the attacker as the victim: personal data, saved payment context, loyalty balances, and the surprisingly intimate AI chat history that a shopping assistant accumulates. On a super-app that fuses commerce, payments, and identity, "log in as anyone" is about as bad as it gets.

The fix

Stop letting a boolean carry authority. Concretely:

  • Bind session issuance to server-verified OTP state. customer-lookup must refuse to mint cookies unless the server itself recorded a successful, single-use verification for that email — ideally by requiring a short-lived, signed verification token the server produced, not a flag the client asserts.
  • Make verification one-time and stateful server-side. Consume the OTP challenge on success so a stale "verified" belief can't be replayed.
  • Keep the flow decisions on the server. The frontend can render UI off {"success":...}, but no privileged action should ever be reachable purely because the client thinks it should be.

Rate-limiting the code was the right instinct. It just guarded the wrong door.

authentication-bypassaccount-takeoverclient-side-trustotpburp-suite

All identifiers, targets and payloads in this post are anonymized or defanged. Findings were reported and resolved through responsible disclosure.