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:
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
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:
{
"data": {
"poeDeviceContactForPhoneNumbers": [
{
"poeUser": {
"uid": 3140577186,
"handle": "exampleuser",
"fullName": "John Doe",
"bio": "...",
"profilePhotoURLSmall": "https://...",
"profilePhotoURLMedium": "https://..."
},
"contactPhoneNumber": "+34600000001"
}
]
}
}
| Field | Description |
|---|---|
uid | The user's unique ID on the platform |
handle | Public username |
fullName | Real full name |
bio | Profile biography |
profilePhotoURLSmall | Profile photo URL (low resolution) |
profilePhotoURLMedium | Profile photo URL (medium resolution) |
contactPhoneNumber | Phone 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:
- Iterates over a configurable range of numbers (sequential or random).
- Groups the numbers into batches of the configured size.
- Sends each batch to the GraphQL endpoint with the session cookies.
- Logs every match found in real time to a CSV.
Main parameters:
| Parameter | Description | Default |
|---|---|---|
--mode | sequential (full scan) or random (sampling) | sequential |
--prefix-range | Range of prefixes to enumerate, e.g. 636 636 | — |
--batch | Phone numbers per request | 50 |
--delay | Seconds between requests | 1.0 |
--output | Output CSV filename | users_spain.csv |
--resume | Resumes from the last saved state | false |
PoC command:
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
| Field | Value |
|---|---|
| Vulnerability type | IDOR + Missing Rate Limiting + Missing Input Validation |
| CWE | CWE-359 — Exposure of Private Personal Information |
| Affected endpoint | POST https://api.target.tld/api/gql_POST |
| GraphQL operation | PoeDeviceContactForPhoneNumbersQuery |
| Authentication required | Yes — a free account |
| Victim interaction | None |
| Exploitation complexity | Very 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.
Related labs
Practice GraphQL endpoint enumeration, batching and PII hunting at scale: GraphQL labs.
Practice this in a lab
Graphql
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
Account enumeration via LinkedIn + phone — exposure of millions of users
Real case: API account enumeration combined with LinkedIn scraping + reverse phone lookups to correlate real identities with private accounts. Critical privacy impact.
URL shortener as a mass PII leak — extraction of ~300 people/hour
Low-entropy codes + no rate limiting + a ticket with no auth = enumeration of phone numbers, cards (BIN+last 4) and real customers' purchases.
IDOR in a newsletter API — the UI hides it, the API hands it over
An endpoint with no server-side authorization check. A public parameter in the URL. Access to the subscriber list of any newsletter on a professional social network.