Mastering API pentesting: Part 2 - business logic, rate limits, GraphQL
API4:2023 (resource consumption) and API6:2023 (sensitive business flows) are where the most expensive bugs hide. Plus GraphQL aliasing, depth attacks, and introspection - the failure modes scanners cannot find.
Where Part 1 left off
Part 1 covered inventory, authentication, and authorization - the OWASP API Top 10 categories API1, API2, API5, and API9. Part 2 picks up the more interesting territory: API4 (resource consumption), API6 (business flows), and GraphQL-specific attack surface.
Business logic flaws (OWASP API6:2023)
These are the findings clients pay us for. They don't show up in scanners because they require understanding what the API is supposed to do. The OWASP API Top 10 added "Unrestricted Access to Sensitive Business Flows" as a new category in the 2023 edition specifically to elevate this class.
Scanners cannot find these. Business-logic flaws require an understanding of intent. A negative quantity is a valid integer; a parallel request is a valid HTTP call. Only a human with context spots them. This is why OWASP added API6 as a standalone category.
The three patterns we look for
Race conditions
Double-spend on a credit transfer, double-discount on a checkout, duplicate redemption of a one-time code. Issue 20-50 parallel requests, watch for inconsistent state. Tools like Burp's "Turbo Intruder" or PortSwigger's single-packet attack technique. Documented in PortSwigger's Web Security Academy.
State machine skipping
Many APIs implement workflows (order → paid → shipped). The vulnerable ones don't enforce transitions on the server. You skip "paid" and POST directly to "shipped". OWASP API6:2023 calls these out by name.
Quantity / pricing manipulation
Submit negative quantities. Submit prices that should be server-determined. Submit fractional values on integer fields. Surprisingly often, the server trusts the client.
# State-skip example - the order workflow
POST /api/orders/12345/ship HTTP/1.1
Authorization: Bearer <our-token>
# Expected: 409 Conflict - order not yet paid
# Observed:
HTTP/1.1 200 OK
{"status": "shipped", "tracking": "..."}
Rate-limiting bypass (OWASP API4:2023)
API gateways rate-limit by API key, by IP, by user. The bypass surface is wider than most teams think.
| Bypass vector | OWASP API4 sub-category | What it costs to fix |
|---|---|---|
| IP rotation via residential proxy | Always available | MED - device fingerprinting |
| API-key signup at scale | When signups are free | LOW - per-account limits + KYC |
| Endpoint version gap | /login limited, /login/v2 not |
LOW - unify config |
| Header bypass | X-Forwarded-For trusted |
LOW - strip at edge |
| Async / batch endpoints | Counted as one call | MED - count inner ops |
GraphQL-specific surface
GraphQL adds attack surface that REST APIs don't have. The OWASP GraphQL Cheat Sheet is the reference for hardening; the four patterns below are what we test for on every engagement.
Introspection in production
Is __schema enabled in production? It usually shouldn't be. Most teams forget to disable it post-launch because every dev needs it locally. The GraphQL spec itself recommends disabling in production.
Query depth attacks
Deeply nested queries that explode server cost. user { friends { friends { friends { friends { name } } } } } can fan out to thousands of resolver calls. Apollo Router supports max-depth caps; most homegrown gateways don't.
Aliasing for batch abuse
Request the same field 1000 times in one query via aliases. One HTTP call, thousands of resolver invocations. Bypasses naive rate-limiting completely - this is the GraphQL equivalent of the rate-limiting batch bypass.
Field-level authorization gaps
Resolver-level auth is often inconsistent. The top-level query is gated; the nested field isn't. order.customer.email leaks even when customer directly is denied. Apollo's documentation calls this out by name.
# Aliasing abuse - 100 lookups in one HTTP call
query {
a1: user(id: 1) { email }
a2: user(id: 2) { email }
a3: user(id: 3) { email }
# ...
a100: user(id: 100) { email }
}
# One request to the rate-limiter; 100 DB lookups behind it.
The GraphQL findings that hurt are never schema-level. They're resolver-level: the field that wasn't gated, the alias that wasn't counted, the depth that wasn't capped. The schema looks safe; the runtime isn't.
- Staatse API retrospective, 2024
Engagement breakdown
Where the time goes on a typical 2-week API engagement.
Key takeaways
- Business logic (OWASP API6:2023) is where the most expensive bugs live - scanners cannot find them.
- Rate limiting (API4:2023) must be tested with actual abuse vectors (IP rotation, header spoof, version gap) - configured doesn't mean enforced.
- GraphQL introspection should be off in production. Depth + alias caps must be enforced at the gateway (Apollo Router supports both), not relied upon at the schema.
- Field-level authorization needs the same rigour as top-level - the nested field is where the data leaks.
Closing
API engagements are some of our most useful per-hour-of-work findings, because the patterns are consistent across customers. If you're shipping a new API and want a structured pre-launch review, our web/API engagement covers this.
References & further reading
- OWASPAPI4:2023 - Unrestricted Resource Consumption
- OWASPAPI6:2023 - Unrestricted Access to Sensitive Business Flows
- GraphQLGraphQL specification - introspection
- OWASPGraphQL Cheat Sheet
- PortSwiggerWeb Security Academy - GraphQL API vulnerabilities
- PortSwiggerRace conditions: web security academy
- Apollo GraphQLSecuring your GraphQL API