One intercepted request. One parameter swap. Full access to any account on the platform.
Press enter or click to view image in full size
I was testing a shopping platform for authentication vulnerabilities.
Standard scope. Phone number login, OTP over SMS, the kind of flow that runs on hundreds of apps right now. I entered my number, got my OTP — 8350 — opened Burp Suite, and intercepted the validation request before forwarding it.
I was looking for a bypass. Null OTP, expired token replay, missing rate limit, the classic 000000. I expected to fail ten different ways before finding anything.
Then I looked at the request body:
Press enter or click to view image in full size
almost went straight for the OTP field. Then I stopped.
The phone number was in the request body. Not from a server-side session. Not from a cookie tied to the OTP initiation step. Right there, client-controlled, in the JSON body. The client was telling the server: “this is the number I’m authenticating for.”
I had a real OTP. And if the server was trusting the phone number from the body to decide which account to authenticate into — I didn’t need to bypass anything.
I changed the phone number to a victim’s number. Kept the OTP as 8350. Forwarded the request.
When people talk about OTP bypass, they mean skipping the OTP entirely. A rate-limiting gap that lets you brute force six digits. A predictable token algorithm. A race condition window. An endpoint that silently accepts a blank value.
Those are bypasses. What I found was not one.
There’s a specific kind of exhaustion that comes with testing auth on platforms that look secure from the outside. You run the obvious attacks, nothing fires, and you start assuming the implementation is solid. That assumption is the most dangerous thing you bring to a test.
OTP authentication creates a strong psychological sense of security — for users and for developers. Two factors: your phone number (your identity) and the code you physically received. The implicit assumption in every OTP flow is that the code only works for the number it was sent to.
That assumption only holds if the server enforces the binding.
If the server trusts the client to report which number is being authenticated, the second factor stops protecting the first. It just adds friction for legitimate users.
After swapping the phone number in the body to a victim’s number and forwarding, the server responded
Full JWT tokens. Access token and refresh token. The victim’s phone number echoed back in the response confirming whose account I was now authenticated into.
I opened the platform with those tokens.
The profile page showed a verified account — the victim’s number, their name, their complete order history, saved delivery addresses, and payment methods on file. I had never interacted with that account. I had never received a code for that number. I just told the server which account I wanted.
It gave it to me.
A correctly implemented OTP flow works like this:
The critical step is five. The server holds the ground truth: “we sent OTP X to phone Y.” The client can’t touch that. The client can only prove they received it by submitting the correct code. The server validates the code against its own record — not against anything the client sends.
Join Medium for free to get updates from this writer.
This platform had cut that link.
The endpoint was accepting phone from the request body and using it to look up which account to authenticate into. The OTP was being validated — it was a real, unexpired code — but the validation was not bound to a server-side session that tracked which number the OTP was issued for. The server confirmed: "this OTP is valid." Then it authenticated into whatever account matched the phone number the client supplied.
Two separate operations that should have been one inseparable check.
The analogy: A hotel that issues key cards on check-in. At checkout, you hand over any valid key card and say “I’m in room 412.” The front desk verifies the card is authentic. Then they charge room 412. They never checked whether that card was issued for 412 — only that it was a genuine card from their system.
Your card opens room 412. Not because you’re a guest there. Because you said you were.
The first pushback I expect: an attacker still needs a valid OTP, so doesn’t that limit the attack surface?
No. Here’s exactly why.
A valid OTP requires one thing: owning any phone number registered on the platform. You initiate login with your own number, receive your OTP, intercept the validation request before it fires, swap your number for any victim’s number, forward it.
Your OTP is valid. The server confirms it. You’re now authenticated into the victim’s account.
The attack doesn’t require intercepting the victim’s messages. It doesn’t require access to their device. It doesn’t require their credentials. It requires their phone number — which on a shopping platform surfaces constantly in order confirmation emails, delivery tracking links, referral codes, and support interactions.
The vulnerability wasn’t clever. That was the point — it didn’t need to be.
Every registered user is simultaneously a potential attacker against every other registered user. One attacker, one valid OTP, any target. The scale is limited only by how many phone numbers you can collect — and on a shopping platform, that’s not a meaningful limit.
Account takeover on a shopping platform isn’t just a profile page. The authenticated session surfaced:
On platforms where checkout is one-click with saved payment methods, the access doesn’t stop at data exposure. Depending on the payment confirmation flow — and whether that flow also uses this endpoint — it can extend to transactions.
I tested the authentication flow. I confirmed the takeover. I stopped there.
Authentication security spends most of its attention on hard problems — cryptographic strength, token entropy, algorithm selection, transport security.
None of those were the issue here.
The failure was in trust assignment. The server trusted the client to correctly identify which account was being authenticated for. That trust had no server-side verification. The OTP was valid. The mechanism ran normally. The wrong person got in. And everything looked correct.
If you were on this same test and intercepted this same request — would your first instinct have been to go after the phone number field, or straight to the OTP?
Be honest with yourself before you answer. I went for the OTP first.
Drop your instinct in the comments — I want to know if this is a common blind spot or just mine.