# Twitter: private list members leaked via IDOR and timing oracle

Twitter GraphQL ListMembers endpoint leaked private list members; missing rate limit plus x-response-time timing oracle let attackers brute-force private list IDs.

- Payout: Research
- Severity: medium
- Program: Twitter
- Write-up by: RyotaK
- Disclosed: 2020-10-09
- Write-up quality: 8/10
- Original: https://blog.ryotak.net/post/twitter-list-chain/
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-ryotak-net-post-twitter-list-chain
- Tags: area:web domain:web impact:data-leak kind:research platform:research sev:medium tech:graphql tech:rest technique:blind technique:chained vuln:access-control vuln:idor vuln:info-disclosure

**Target:** Twitter GraphQL ListMembers endpoint and REST lists/destroy.json
**Root cause:** ListMembers GraphQL query lacked an ownership/privacy check, returning members of private lists given the list ID; lists/destroy.json had no rate limit and leaked processing time via x-response-time header.
**Steps:**
1. Send `GET /graphql/iUmNRKLdkKVH4WyBNw9x2A/ListMembers?variables={"listId":"<id>","count":20,...}` with OAuth2Session bearer to read members of a private list.
2. Observe `POST /1.1/lists/destroy.json` with `list_id=<id>` returns 404 for others' private lists but has no rate limit.
3. Compare `x-response-time` header: existing private list vs nonexistent list differs by ~20 units.
4. Brute-force list IDs using the timing oracle at unlimited rate to enumerate private list IDs.
5. Feed recovered IDs into ListMembers to dump private membership.
**How found:** Noticed ListMembers returned private list contents; Twitter argued list IDs are hard to obtain, so researcher analyzed Snowflake ID structure and hunted for an ID oracle, finding undocumented missing rate limits and the x-response-time header leaking processing time.
**Exploitation:** Attacker brute-forces Snowflake-style list IDs (timestamp+worker+dc+counter) using the unthrottled destroy endpoint's x-response-time timing side channel, then queries ListMembers with the recovered ID to read private list members.
**Escalation:** Chained IDOR with missing rate limit and timing side channel to make ID enumeration practical, converting a 'potential' bug into a demonstrable one.
**Insight:** When a vendor dismisses an IDOR because the identifier is 'unguessable', look for a side channel that confirms identifier existence (here a timing header on an unthrottled endpoint) to turn it into a practical enumeration primitive.
