HighIDOR· 3 min read

Same ID, Different Verb, Someone Else's Data

One endpoint checked who you were. Its next-door sibling, sharing the same service layer, forgot to.

Target anonymized · a retail loyalty platform

Access-control bugs love seams — the places where two pieces of code that were written at different times meet. This loyalty platform had a clean seam right in its API naming.

The spark

Viewing my own account, the app called something like /getBillingData/{my-id} and returned my details, properly scoped to me. Fine. But APIs like this rarely have just one method — so I went looking for its relatives.

Digging in

A sibling method existed: swap the verb in the path to a related "entity" fetch and pass the same ID. It returned data too — except this one didn't run the ownership check its neighbour did. Both endpoints ultimately hit the same service layer, but only the first one had remembered to ask "is this yours?"

I ran the false-positive gate: a garbage ID returned an error, so the endpoint genuinely resolved the value. Then I put a victim's ID on the unguarded sibling.

GET /api/getCLEntity/{victim-id} HTTP/2
Host: app.example.com
# same session as me — returns the victim's personal data

The guarded endpoint gave everyone a false sense of safety. The bug was the door right next to it that nobody thought to lock.

Impact

Cross-user disclosure of personal data — read any user's details by knowing (or enumerating) their identifier, through the sibling endpoint that skipped the check.

The fix

Enforce authorization once, in the shared service layer that both endpoints call, so a new route can't accidentally inherit data access without inheriting the check.

idorbroken-access-controlpii

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