Quick answer
Account enumeration looks like a "low severity bug" in shallow reports. But the chain — enumerating accounts in a private API + correlating with public LinkedIn + reverse phone lookup → you turn "private user" into "Juan Pérez, CTO of XYZ, mobile +34 6XX XXX XXX, wife María". Massive privacy impact. The bug class that generated the most big bounties in 2026 in dating, fintech, health, and networking apps. Reported to 5+ unicorns with combined bounties >€80K. Medium-high severity when you frame it with GDPR/CCPA/PIPL regulation.
The framing — why it matters beyond "user exists"
A typical enumeration response:
GET /api/user/check?email=victim@gmail.com
{"exists": true}
Shallow reports: "Severity Low, just informational". Reward: $50-$500.
The correct framing: enumeration + correlation = de-anonymization. Apps with "private" users (dating, mental health, recovery, activism) → mapping real identity to an "anonymous" profile is a Critical privacy violation.
| Without chain | With chain (correlation) |
|---|---|
| "victim@gmail.com exists" | "victim@gmail.com is Juan Pérez, CTO at XYZ, father of 2, lives in Madrid" |
| Severity Low | Severity High/Critical |
| $50-$500 | $2K-$15K |
Pattern 1 — enumeration via /forgot timing
Endpoint POST /forgot { email }:
victim@gmail.com → 200 OK in 850ms ("email enviado si existe")
random@test.tld → 200 OK in 120ms ("email enviado si existe")
Same response body, different timing. The real email runs more code (DB lookup, token generation, email send) than the fake one → a measurable difference.
Automated detection
import requests, time
def time_request(email):
start = time.time()
requests.post("https://target.com/forgot", json={"email": email})
return time.time() - start
print(time_request("victim@gmail.com")) # 0.85s
print(time_request("fake_user_xx@gmail.com")) # 0.12s
After 20 control samples, distinguishing 200ms from 800ms is trivial.
The mitigation that breaks
Devs usually "fix" this by adding setTimeout(800ms) to the fake endpoint → but they forget there is a real setTimeout → the variance distinguishes 800±50ms (artificial) from 800±200ms (natural).
Pattern 2 — enumeration via different response codes
POST /signup
{"email": "exists@target.com", "password": "x"}
→ 400 "Email already in use"
POST /signup
{"email": "new@target.com", "password": "x"}
→ 200 "OK"
Trivial. But less obvious:
POST /api/2fa/send-otp
{"email": "exists@target.com"} → 200 (otp sent)
{"email": "new@target.com"} → 200 (otp sent, fake)
Same response — but only the real one generates an audit log, SMS cost, and per-user rate limit. Pivot to side channels: is there a counter of "X emails sent" visible in some admin/billing endpoint? Every call to the first one increments, the second doesn't.
Pattern 3 — GraphQL introspection + enumeration
GraphQL multiplies the surface. Partial search queries → enumeration friendly:
query {
searchUsers(query: "victim", limit: 50) {
edges {
node {
id
username
# campos sensibles según el schema
publicProfile {
fullName
jobTitle
company { name }
}
contactInfo {
email
phoneNumber
}
}
}
}
}
Test:
- Introspect the schema (if exposed):
query { __schema { types { name fields { name } } } }. - List fields on
User. There are often scope-protected fields that leak by mistake. - Fire
searchUserswith short queries (1-2 chars) → extensive results.
Typical bug
User.email and User.phoneNumber are "protected" in the direct query user(id) (it checks ownership) but not in searchUsers (it returns the list without a per-field check) → mass leak.
Pattern 4 — chain with LinkedIn scraping
The real severity jump. After enumerating 10K emails on the target:
Step 1 — bulk LinkedIn lookup
LinkedIn allows contact import by email. If you control a LinkedIn account with a moderate professional network, you can:
- Upload a CSV of emails → LinkedIn returns "these are on LinkedIn, these aren't".
- For the ones that match: full name + current company + role + location + photo.
- Tools:
Hunter.io,Apollo.io,ZoomInfo,Lusha,Snov.io— all consume an email → return an enriched profile.
Step 2 — reverse phone lookup
If the target.com API exposes phone:
Truecaller(web search, no public API but scraping doable)WhitePages.com(US)Yellow.es(Spain)Showcaller/Hiya- Other aggregator services
Result: anonymous number → "Juan Pérez, Madrid, age ~35".
Step 3 — correlation engine
For each enumerated user:
target_id: 184729
email: jperez@gmail.com
phone: +34 6XX XXX XXX
→ LinkedIn lookup:
name: Juan Pérez
company: XYZ Corp
title: Senior Engineer
location: Madrid, ES
→ Reverse phone:
carrier: Movistar
region: Madrid centro
public listings: Juan Pérez (esposa María)
→ Combined dossier:
{full_name, employer, title, family, location, target_userId}
→ Now the "anonymous user 184729" on target.com is Juan Pérez with all his data. This is the attack that privacy regulations classify as severe — a user who thought they were using the app "anonymously" is completely identified.
Pattern 5 — phone + LinkedIn email reset chain
Documented in the report "An attacker forgets your email and LinkedIn gives him your phone number":
- The attacker finds that your public LinkedIn shows "+34 6XX..." in the bio (common in freelancer and consultant profiles).
- The target.com app allows "forgot password" via an alternative phone number:
http
POST /auth/forgot-by-phone {"phone": "+34 6XX XXX XXX"} - If the app doesn't require other factors → SMS reset to the attacker's phone (SIM-port if you got a swap, or if the target SMS arrives at a phone made visible via LinkedIn).
Severity
When this includes recovery via the attacker's phone (not the victim's) → ATO. When it only allows discovering the victim's phone → high privacy violation.
Pattern 6 — bulk extraction without triggering rate limit
"Informational" endpoints usually have a relaxed rate limit (100 req/min) because "no sensitive leak". But if you leak phone/email/PII → lax rate limit × millions of IDs = bulk extraction.
Techniques to avoid triggering the rate limit
- Distributed IPs: residential proxy network (legal: ProxyMesh, Bright Data; bug bounty use: NO).
- Pagination with offset:
?limit=100&offset=N— each request is "different" in the logs. - Field selection (GraphQL): only request the sensitive fields, avoid "expensive" fields that trigger monitoring.
- Off-peak time: run the attack in the target's local early morning → less manual detection.
[!warning] Bug bounty scope Do NOT run bulk extraction against real users for a PoC. Most programs explicitly forbid it — extract 5-10 records from YOUR accounts + 1-2 control ones (that you know exist), document the pattern. The report with "and here are 50K real extracted records" usually results in a ban from the program.
Reporting framework
The report has to narrate the chain:
## Summary
Combined account enumeration in /api/forgot + bulk lookup via /graphql
searchUsers allows correlating private user accounts with full real-world
identities via LinkedIn email lookup.
## Steps
1. Enumeration: POST /api/forgot exhibits timing oracle (850ms real vs 120ms fake)
2. Bulk: GraphQL searchUsers(query) returns phone+email without ownership check
3. Correlation: LinkedIn email lookup (CSV import) yields full name + employer
## Impact
"Anonymous" user IDs 100% de-anonymizable. PII covered by GDPR Art. 6/9.
Specific cases (mental health, dating apps, recovery): severity Critical.
## Demonstration
Attached: 3 test accounts I control + 1 control account explicitly verified
with the program team. Total records: 4. Method generalizable to entire dataset.
Report the full chain and not each piece separately. Individual piece = $200; chain = $5K-$15K.
Hunting checklist
- Endpoints where the response gives away a user's existence: forgot, signup, login error messages, 2FA send.
- Timing oracle: measure real vs fake time (10 samples each).
- GraphQL: introspection + searchUsers / nodes by partial filter.
- Bulk APIs: contact import, batch lookup, suggested friends.
- Sensitive fields in the response: email, phone, full name, address, jobs.
- Pagination:
?offset=Nfor incremental extraction without a spike. - LinkedIn lookup chain: does the target have a public email or phone?
- Reverse phone: if the API leaks phone, is it correlatable with a real identity?
- Privacy framing: identify the applicable regulation (GDPR, CCPA, LGPD).
- Report with a narrative chain + minimal PoC (3-5 records).
Related labs
Practice enumeration timing, GraphQL bulk extraction and PII correlation in enumeration and PII labs.
Practice this in a lab
Mass Pii Extraction Graphql Enumeration
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
La query GraphQL exacta que extrae 100k usuarios con email + phone + employer en <2h sin trigger rate limits.
€7.99/mo · cancel anytime
Related articles
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.
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.