Advanced levelFree

Mass PII Extraction via GraphQL — 93 real profiles in 1 hour

A contact-sync GraphQL endpoint with no rate limiting, no ownership verification and batching of 200 numbers per request. Phone → real identity resolution.

Gorka El BochiMay 9, 202613 min

Quick answer

A "contact sync" GraphQL endpoint on a conversational AI platform accepted arbitrary arrays of phone numbers per request, with no rate limiting and without verifying they belonged to the user's address book. PoC: enumeration of the +34 636 prefix (a million candidates) in 1 hour, 93 real profiles extracted with UID, handle, full name, bio and photos. Zero detections.


1. Context — the contact sync feature

The platform's mobile app includes a sync feature: the user can upload the numbers from their address book and the platform returns which numbers correspond to registered accounts. This feature is exposed as a GraphQL endpoint in the API.

The endpoint is:

arduino
POST https://api.target.tld/api/gql_POST
Operation: PoeDeviceContactForPhoneNumbersQuery

The bug is that this endpoint:

  • Has no access control beyond having an authenticated session.
  • Doesn't verify that the sent numbers belong to the device's address book.
  • Has no rate limiting.
  • Accepts arrays of hundreds of numbers per request.

2. The vulnerable request

http
POST /api/gql_POST HTTP/2
Host: api.target.tld
Cookie: m-b=...; m-login=1; m-s=...; m-uid=...
Content-Type: application/json

{
  "extensions": {
    "hash": "c56e7237b4ac9258dc9dd8633a4165ba386052e8e0ed964b5c9515003e58139c"
  },
  "operationName": "PoeDeviceContactForPhoneNumbersQuery",
  "variables": {
    "phoneNumbers": ["+34636138342", "+34636180219"]
  }
}

The phoneNumbers field accepts an array of arbitrary phone numbers — they don't have to be in the device's address book or have any relationship with the authenticated user.


3. The response — PII returned for each match

For each number that corresponds to a registered account, the endpoint returns:

json
{
  "data": {
    "poeDeviceContactForPhoneNumbers": [
      {
        "poeUser": {
          "uid": 3140577186,
          "handle": "exampleuser",
          "fullName": "John Doe",
          "bio": "...",
          "profilePhotoURLSmall": "https://...",
          "profilePhotoURLMedium": "https://..."
        },
        "contactPhoneNumber": "+34600000001"
      }
    ]
  }
}
FieldDescription
uidThe user's unique ID on the platform
handlePublic username
fullNameReal full name
bioProfile biography
profilePhotoURLSmallProfile photo URL (low resolution)
profilePhotoURLMediumProfile photo URL (medium resolution)
contactPhoneNumberPhone confirmed as linked to that account

The result is a direct, confirmed mapping of phone number → real identity.


4. Features that amplify the impact

No rate limiting. During the PoC, requests were sent continuously for hours against the +34 636 prefix (1,000,000 candidate numbers) without hitting any throttling, block, CAPTCHA or progressive delay. Every request returned successfully.

Massive batching. The phoneNumbers field accepts arrays of up to 200 numbers per request (tested in the PoC). This allows enumerating large ranges with very few HTTP requests.

No ownership verification. The endpoint doesn't check that the sent numbers exist in the address book of the device making the request. Any arbitrary number is valid as input.

No prior relationship required. No prior interaction is needed between the attacker and the victims whose data is extracted.


5. The automation script

To demonstrate the impact at scale, a Python script was developed that automates the full enumeration of any phone prefix in any country.

How it works:

  1. Iterates over a configurable range of numbers (sequential or random).
  2. Groups the numbers into batches of the configured size.
  3. Sends each batch to the GraphQL endpoint with the session cookies.
  4. Logs every match found in real time to a CSV.

Main parameters:

ParameterDescriptionDefault
--modesequential (full scan) or random (sampling)sequential
--prefix-rangeRange of prefixes to enumerate, e.g. 636 636
--batchPhone numbers per request50
--delaySeconds between requests1.0
--outputOutput CSV filenameusers_spain.csv
--resumeResumes from the last saved statefalse

PoC command:

bash
python3 phone_enum.py --mode sequential --prefix-range 636 636 --batch 200

Output CSV: columns phone, uid, handle, fullName, bio, profilePhotoURLSmall, profilePhotoURLMedium, timestamp.


6. Real PoC results

Enumeration against the Spanish +34 636 prefix (1,000,000 candidates):

  • Runtime: ~1 hour.
  • Candidate numbers sent: 1,000,000.
  • Real user profiles extracted: 93.
  • Rate limiting encountered: none at any point.
  • Blocks or detections: none.

The results CSV includes the 93 profiles with phone, UID, handle, full name, bio and photos. The researcher's own profile appears in the results, confirming correct scan coverage.

+34 636 is one of the 190 prefixes available in Spain alone. The script applies directly to any country in the world by changing the prefix parameter.


7. Observed impact

  • Extraction of 93 real profiles with full PII in 1 hour of scanning over a single prefix.
  • Confirmed mapping of phone → real name, UID, handle, bio and profile photos.
  • Zero defensive mechanisms triggered during the entire process.
  • The same script works against any prefix in any country without modifications.

8. Technical classification

FieldValue
Vulnerability typeIDOR + Missing Rate Limiting + Missing Input Validation
CWECWE-359 — Exposure of Private Personal Information
Affected endpointPOST https://api.target.tld/api/gql_POST
GraphQL operationPoeDeviceContactForPhoneNumbersQuery
Authentication requiredYes — a free account
Victim interactionNone
Exploitation complexityVery low — an HTTP request with an array of phone numbers

9. Technical notes

  • The endpoint uses a fixed hash to identify the GraphQL operation: c56e7237b4ac9258dc9dd8633a4165ba386052e8e0ed964b5c9515003e58139c. This hash is static and reproducible.
  • The required session is obtained from the mobile app's cookies captured with a proxy (Burp Suite in the PoC).
  • The 200-number batch per request was the size tested in the PoC — the upper limit may be higher.
  • The response only returns data for numbers that actually have an account. Numbers without an account don't generate an error, they simply don't appear in the response array.

Practice GraphQL endpoint enumeration, batching and PII hunting at scale: GraphQL labs.

Practice this in a lab

Graphql

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy