Intermediate levelWith account

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.

Gorka El BochiMay 11, 202614 min

Stop reading about bugs and start hunting them

No card · free Academy · cancel anytime

Create free account

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:

json
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 chainWith chain (correlation)
"victim@gmail.com exists""victim@gmail.com is Juan Pérez, CTO at XYZ, father of 2, lives in Madrid"
Severity LowSeverity High/Critical
$50-$500$2K-$15K

Pattern 1 — enumeration via /forgot timing

Endpoint POST /forgot { email }:

graphql
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

python
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

http
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:

http
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:

graphql
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:

  1. Introspect the schema (if exposed): query { __schema { types { name fields { name } } } }.
  2. List fields on User. There are often scope-protected fields that leak by mistake.
  3. Fire searchUsers with 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.

<!-- PAYWALL -->

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:

yaml
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":

  1. The attacker finds that your public LinkedIn shows "+34 6XX..." in the bio (common in freelancer and consultant profiles).
  2. The target.com app allows "forgot password" via an alternative phone number:
    http
    POST /auth/forgot-by-phone
    {"phone": "+34 6XX XXX XXX"}
    
  3. 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:

markdown
## 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=N for 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).

Practice enumeration timing, GraphQL bulk extraction and PII correlation in enumeration and PII labs.

Free · no account

The checklist I run on every new target

47 checks ordered by cost: first what can get you in trouble, then the cheap stuff, and finally the expensive stuff — which is where the big bounties are. I'll send it to your inbox right now.

Unsubscribe in one click, from any email.

Practice this in a lab

Mass Pii Extraction Graphql Enumeration

Solve

Keep learning · free account

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

Create account
Premium · 1 more technique

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.

Unlock

€7.99/mo · cancel anytime

Related articles

4,183

hunters training

68

labs from real hacks

2,125

completions

$14,790

paid out for these bugs

6 flags captured this week·Real hacks from HackerOne · YesWeHack · Bugcrowd·No commitment·Free Academy

The community

This isn't a platform with users.It's people breaking the same things you are.

Inside people ask, share what they found and solve together what would block anyone on their own. It's free to join: no account, no plan, no email required.

Join the BBLABS community on Discord
Join the Discord

Free and open. You don't need a BBLABS account to join.