Read-Path Auth Bypass on a Live Auction API
- Broken object-level authorization
- Sensitive data exposure
Read-Path Auth Bypass on a Live Auction API#
Vulnerability Research — Case Study
An auction feature on a telecom e-commerce platform let anyone watch live bidding — and years of bid history — without logging in. The write path was properly locked down. The read path wasn't, and the two didn't agree on who was allowed to look.
Role: Independent security researcher Class: Broken object-level authorization (CWE-284) · Sensitive data exposure (CWE-200) Severity: Medium · CVSS 5.3 Status: Reported, fixed, disclosed with permission
The target's identity, exact endpoints, and all real customer data (phone numbers, names, user IDs, auction IDs, timestamps) have been removed or replaced with illustrative placeholders. Nothing below can be replayed against the live system.
01 — Context#
The platform ran a phone-number auction feature: premium MSISDNs (phone numbers) go up for bid, users place sealed-looking bids through an authenticated API, and the highest bidder wins. Placing a bid correctly required a session. Reading the state of an auction did not.
02 — The asymmetry#
Two endpoints, one resource:
| Endpoint | Purpose | Auth required? |
|---|---|---|
make_bid | Place a bid | Yes — returns 401 without a session |
get_auction_details | Read an auction's state and bid history | No |
A plain POST with nothing but an auction_id returned the full record: the phone number being auctioned, every bid placed against it, and — for auctions still running — bids arriving in real time as other users placed them.
POST /auction/api/v2/get_auction_details
Content-Type: application/json
{"auction_id": <id>}
→ 200 OK, no auth header, no cookie
{
"auction": {
"msisdn": "<phone number>",
"status": "in_bidding",
"bids": [
{"bidder_id": <id>, "user_first_name": "<name>", "price": <amount>, "status": "winner_first", "created": "<timestamp>"},
...
],
"max_bid_user_id": <id>,
"no_participants": <count>
}
}Auction IDs were sequential integers, so the entire history of the feature — not just the auctions currently open — was walkable from 1 to the current high watermark.
03 — What that exposed#
Per bid: an internal user ID, the bidder's first name, the amount bid, a timestamp, and whether that bid won or lost.
Per auction: the phone number up for sale, the current high bid and bidder, and participant counts — live, as bids came in, for auctions still open.
Across the whole ID range: every auction the feature had ever run, going back years, with the same level of bidder detail.
None of this required a credential. It required knowing the feature existed and that auction IDs were sequential — both true the moment you load the page.
04 — Why this is more than "it's just product info"#
The team's likely reasoning for leaving this unauthenticated is easy to guess: auction listings are a product feature, and showing that an auction exists and what it's going for is reasonable to make public, the way a public eBay listing shows current price without showing bidder identities.
The bug is that "auction state" and "who is bidding and by how much" got bundled into the same response with the same access control. A caller who should only be able to see "this number is at 16.8M Rials, 13 people have bid" could also see exactly who placed which bid, in what order, under their own name and internal ID. That's the difference between a public listing and a leaked transaction ledger.
The contrast with make_bid returning a clean 401 makes clear this wasn't a deliberate policy — the write path was built with authorization in mind and the read path wasn't.
05 — Impact#
- Real-time bid surveillance. A participant in a live auction could watch competing bids arrive, identify who was bidding against them and for how much, without ever authenticating or placing a deposit — a direct advantage in a process meant to be a blind or semi-blind auction.
- Historical financial and identity disclosure at scale. Every past auction's bid history, with real names and internal user IDs attached to specific bid amounts, was walkable by anyone.
- A pivot point for correlation. Internal user IDs are low-value in isolation but become useful once tied to a name and a bid amount — exactly the kind of cross-referenceable identifier that turns a "minor" leak into deanonymization fuel when combined with data from elsewhere.
06 — Root cause and fix#
The read endpoint was never wired through the same authentication/authorization middleware as the write endpoint for the same resource. That's a routing-and-review gap more than a logic bug: whoever added get_auction_details either assumed the resource was meant to be fully public, or didn't realize the response model included bidder PII by default.
Recommended remediation, in order of preference:
- Require authentication on the read endpoint, matching the write path.
- If part of the response is meant to be public (current price, time remaining, participant count), split the response: return the public aggregate fields to anyone, and gate the bid-level fields (
bidder_id,user_first_name, individual bid records) behind a session. - If showing bidding activity is a deliberate product choice, show it pseudonymously — "Bidder #3" rather than a real first name tied to a real internal ID.
07 — Takeaway#
Authorization bugs don't need a missing login screen to exist. This one had a working, correctly-gated write endpoint sitting right next to an ungated read endpoint for the same object. Whenever an API exposes both a read and a write path for the same resource, checking that they agree on who's allowed to touch it is worth doing explicitly — "the write path is secured" says nothing about the read path, and the two are reviewed by different people, at different times, often without either side checking the other.
Reported through the vendor's responsible disclosure process. Published with the vendor's acknowledgment after remediation. — 0xNiemand