← Back to all reports

Account-Summary IDOR Leaks Every Subscriber's Details

Reported May 24, 2026
Severity Critical
Platform Mobile + API
Vulnerability Class Insecure Direct Object Reference (CWE-639)
Target Type Mobile Network Operator (MVNO)
Impact Full PII of the entire subscriber base from one login

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 roster
  • GET /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:

EndpointData exposed
plan/summaryLegal name, active mobile number, plan size and duration, renewal date, suspension status, full family roster
payment/summaryBilling 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.