Severity is a story about impact, not about a single request. This writeup walks through how a finding that a scanner would have labelled "low — information disclosure" became a full account takeover (ATO) once I followed the data instead of stopping at the first anomaly. Details are anonymised; the target and any customer data are omitted under the engagement's disclosure terms.
Where it started
The application was a fairly standard multi-tenant SaaS: users belong to organisations, and each organisation sees only its own orders. While mapping the app I logged every request through my proxy and tagged anything that carried a numeric or sequential identifier. One endpoint stood out:
GET /api/v2/orders/{orderId}
Authorization: Bearer <session-jwt>
Changing orderId to a value that belonged to a different organisation still returned 200 OK with the order body. Classic Insecure Direct Object Reference (IDOR) — the server authenticated who I was but never checked whether this object was mine.
On its own this looked minor. The order object leaked another tenant's shipping city and an item list. Annoying, reportable as a "low", and easy to dismiss. So the interesting question wasn't "is this a bug?" — it was "what exactly is inside the object I can now read?"
Reading the whole object
The habit that pays off here is never trusting the rendered UI to tell you what an API returns. The front-end displayed four fields. The raw JSON returned sixteen. Buried near the bottom:
{
"orderId": 84213,
"status": "shipped",
"customer": {
"email": "[email protected]",
"passwordResetToken": "a1b2c3d4-...",
"resetTokenExpiresAt": "2026-01-21T10:00:00Z"
}
}
The order object embedded the customer's live password-reset token. That is the entire ballgame. A field that had no business being in an order-details response turned an authorization flaw into an authentication bypass.
The chain
Three weak links, each harmless-looking in isolation:
- No object-level authorization on
GET /orders/{id}— I can read any order. - Over-fetching / mass exposure — the order serializer embedded the full customer record, including the reset token, instead of a minimal DTO.
- A replayable reset flow — the public endpoint
POST /reset-passwordaccepted that token with no second factor and no binding to the requesting session.
Put together:
- Enumerate order IDs (they were sequential, so no guessing required).
- For each, pull the embedded
passwordResetToken. - Replay it against the public reset endpoint to set a new password.
- Log in as that user.
A "low-severity information disclosure" was, in practice, account takeover for any user who had ever placed an order. In the report I demonstrated it against a single test account I controlled on the staging tenant — proving the chain without ever touching a real customer.
Why scanners miss this
An automated scanner flags the IDOR (it sees the ID swap return 200) and stops. It has no concept of what the leaked field means. It cannot know that passwordResetToken in an order body is catastrophic while shippingCity is trivia. The escalation lives in human reasoning about the data, and in chaining a second, physically separate endpoint into the first. That gap between "a scanner flags anomalies" and "an attacker reasons about impact" is exactly where the real findings live — and it's the philosophy behind SentryScan, which tries to prove an exploitable path rather than just list anomalies.
Fixes I recommended
- Enforce object-level authorization server-side on every object fetch. The identity in the token is not authorization; ownership must be checked against the requested resource.
- Return minimal DTOs. An order response should never carry authentication secrets. Serialize an explicit allow-list of fields, never the raw model.
- Bind reset tokens to the flow that issued them, expire them aggressively, invalidate on use, and never surface them through any other API.
- Make IDs non-enumerable (UUIDs) as defence in depth — it does not fix the authorization gap, but it removes the free enumeration that made mass exploitation trivial.
Takeaways for testers
- Always test what data an IDOR exposes, not merely that the reference is broken. The reference is the door; the impact is what's in the room.
- Diff the raw API response against the rendered UI on every endpoint. Over-fetching is everywhere.
- Severity belongs to the chain, not the single bug. Two "lows" that touch each other are frequently a "critical".
If you're shipping a multi-tenant product and want this class of chained authorization flaw found before an attacker enumerates it, that's exactly the kind of engagement I run — get in touch.