Account-Summary IDOR Leaks Every Subscriber's Details
The Risk
Anyone with a single ordinary account at this mobile carrier could look up any other customer's full name, phone number, plan and billing details just by changing one number in a web address. There was no limit on how many customers could be pulled, so the entire subscriber list, millions of people, could be downloaded in a matter of hours. The exposed name-and-number pairing is exactly what phone companies use to approve transferring a number to a new SIM card, so this opened the door to hijacking customers' phone numbers and the text-message security codes tied to them. Family plans made it worse, returning the details of every linked person in a single lookup.
The Vulnerability
The carrier's storefront and customer self-service portal both talk to a shared back-end account-summary service. Two endpoints on that service returned a subscriber's account details by numeric account id:
GET /account/{id}/plan/summary- name, mobile number, plan, renewal date, family rosterGET /account/{id}/payment/summary- billing name, amount due, totals, renewal balance, auto-renew status
When a request arrived, the server validated the session token's signature but never checked that the account id inside the token matched the {id} in the URL path. One valid session therefore authorized reads of any account. This is a textbook Insecure Direct Object Reference (IDOR): authentication was enforced, authorization was not.
Two factors turned a single-record leak into a whole-database leak:
- Sequential identifiers. Account ids are consecutive integers. The populated range ran from the low hundred-thousands up into the tens of millions, covering the active subscriber base plus historical accounts.
- No rate limit. A single token could read records back-to-back with no throttling. A 50-record burst from one token completed 49 of 50 reads with zero rejections.
The Attack
The bug-protection layer in front of the account-summary service rejected intercepting proxies, so the cleanest path was to run everything from the browser developer console after a normal login on the customer portal.
Establishing a session
A standard subscriber logs in to the self-service portal. From the page's own context, a direct call to the login endpoint returns an access token in the response body. That token is stashed and reused as an Authorization: Bearer header for every following request. The cookie-based session the front-end maintains is irrelevant; the flaw lives entirely in how the back-end verifies the token.
Reading a stranger's account
A baseline read of the attacker's own account confirms the request shape. Then the only change is the numeric id in the path:
GET /account/{attackerId}/plan/summary -> 200, attacker's own PII
GET /account/{victimId}/plan/summary -> 200, a stranger's full PII The same token, issued for the attacker's account, returns HTTP 200 with a different subscriber's name, mobile number and plan. The server never compares the token's account claim against the id in the URL.
Mass enumeration
Because ids are sequential and unthrottled, a simple loop walks the id space and pulls a row per account: name, mobile number, plan size and family count. Reads across the full range, from the low hundred-thousands up into the tens of millions, all returned live subscriber data to a single token. A linear sweep recovers the entire customer directory in hours.
Family-plan amplification
A lookup against a family-primary account returns a family[] array containing the full name and mobile number of every secondary line on that plan. One request therefore harvests multiple people's details at once.
Billing variant
The sibling payment-summary endpoint shared the identical flaw, exposing billing identity for any account id: display name, amount due, totals, renewal balance, auto-renew status and due date.
The Impact
From one ordinary authenticated session, an attacker could read, for any subscriber in the carrier's base:
| Endpoint | Data exposed |
|---|---|
| plan/summary | Legal name, active mobile number, plan size and duration, renewal date, suspension status, full family roster |
| payment/summary | Billing name, amount due, total, renewal balance, auto-renew status, due date, promotions, family billing records |
The most serious consequence is account takeover beyond the carrier itself. The leaked pairing of legal name plus active mobile number is precisely the authorization data carriers and ported-from carriers use to approve SIM swaps and number ports. With that pairing for every subscriber, an attacker has the pre-condition for hijacking any customer's phone number, and through it, every text-message security code tied to that number. Family-plan reads compound the exposure by returning every linked line in a single response.
Scale is the multiplier here. This is not a leak of one record; it is the full active subscriber base plus historical accounts, retrievable in hours with no special access, no rate limiting, and no alarm.
Remediation
- Enforce an ownership check on every account route: reject the request unless the session token's account id equals the
{id}in the path. A correctly-behaving sibling version of the endpoint already rejected mismatched tokens with HTTP 401; that check should be ported to all variants. - Serve family rosters through an explicit, ownership-checked family route rather than embedding linked-line details in arbitrary account lookups.
- Apply the same ownership check across the entire family of
/account/{id}/*endpoints, not just the two confirmed here. - Add per-token and per-IP rate limiting plus anomaly alerting on these read paths to detect and slow mass enumeration.
- Consider non-sequential, non-guessable account identifiers to remove the trivial enumeration path even if an authorization check is missed elsewhere.