9 min read

REST API Attacks

REST APIs are the backbone of modern web and mobile applications. They expose business logic directly and often lack the same security controls as traditional web interfaces. This guide covers the OWASP API Security Top 10 attack vectors with practical exploitation techniques.

Information

OWASP API Security Top 10 (2023): API1:BOLA, API2:Broken Authentication, API3:Broken Object Property Level Auth, API4:Unrestricted Resource Consumption, API5:Broken Function Level Auth, API6:Unrestricted Access to Sensitive Business Flows, API7:SSRF, API8:Security Misconfiguration, API9:Improper Inventory Management, API10:Unsafe Consumption of APIs.

Frameworks: WSTG-ATHZ-02, -04 · ASVS 5.0 V4.1.4, V8.2.1, V8.2.2 · CWE-284, CWE-639 · T1190

The REST-Specific Surface

REST's attack surface is not the JSON — it is the routing contract around it. A resource is addressed by a path, acted on by a verb, described by a content type, and versioned by a prefix; each is a separate decision the framework makes. When the router, the authorisation filter and the serialiser disagree, the gap is the bug. BOLA, BFLA and mass assignment have their own pages; this covers the surface specific to REST.

Everything here needs one working request per resource — from the OpenAPI document, the mobile client, or the browser's traffic. Without it a 405 is ambiguous: unsupported verb, wrong route, or an edge rejection.

API Discovery & Enumeration

api-discovery.sh
# Discover API endpoints:

# Check common documentation URLs:
curl -s https://target.com/swagger.json
curl -s https://target.com/openapi.json
curl -s https://target.com/api-docs
curl -s https://target.com/v2/api-docs           # Spring Boot
curl -s https://target.com/swagger/v1/swagger.json # ASP.NET
curl -s https://target.com/.well-known/openapi.json

# Fuzz API paths:
ffuf -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
  -u https://target.com/api/FUZZ -mc 200,201,301,302,401,403,405

# Check for API versioning:
curl -s https://target.com/api/v1/users
curl -s https://target.com/api/v2/users  # May have different auth
curl -s https://target.com/api/v3/users  # Unreleased version?

# Arjun - discover hidden parameters:
arjun -u https://target.com/api/users -m GET
arjun -u https://target.com/api/users -m POST

# Kiterunner - API-aware content discovery:
kr scan https://target.com -w routes-large.kite

Verb and Method Tampering

Router-level method checks are frequently incomplete: the route table allows GET, but the handler behind it accepts whatever the framework binds. Ask the path what it admits with OPTIONS, then try the verbs it did not mention. A true positive is a state change through a verb the UI never sends — a PUT that updates a record, a PATCH that skips validation the POST path enforces. A 405 is a negative.

method-tampering.sh
# What does the route admit?
curl -s -i -X OPTIONS https://target.com/api/users/42 \
  -H 'Authorization: Bearer USER_TOKEN' | grep -i '^allow:'

# Try the verbs the UI never sends
for m in GET POST PUT PATCH DELETE; do
  curl -s -o /dev/null -w "$m %{http_code}\n" -X "$m" \
    https://target.com/api/users/42 \
    -H 'Authorization: Bearer USER_TOKEN' \
    -H 'Content-Type: application/json' \
    -d '{"role":"user"}'
done

Method-override headers are the other half: an edge that only inspects POST may still forward X-HTTP-Method-Override: DELETE to an origin that honours it. Send the request with and without the header and compare effect, because a 200 on both usually means the override was ignored.

Content-Type Confusion

The body parser is chosen by Content-Type, and the authorisation filter usually runs before it. Declaring JSON as text/plain avoids a browser preflight and can bypass a rule written for application/json; sending a form body to a JSON endpoint tests whether the binder falls back. The confirmation is that the origin processed it — a created record, a changed field — not a 200 that merely echoes the request.

content-type-confusion.sh
# Same body, three declared types
curl -s -X POST https://target.com/api/users \
  -H 'Content-Type: application/json' -d '{"name":"probe"}'
curl -s -X POST https://target.com/api/users \
  -H 'Content-Type: text/plain' -d '{"name":"probe"}'
curl -s -X POST https://target.com/api/users \
  -H 'Content-Type: application/x-www-form-urlencoded' -d 'name=probe'

# 415 = rejected cleanly. 201 plus a new record = the binder accepted a type
# the endpoint never advertised.

XML is the same test with a different parser, and worth one request per endpoint: a parser that resolves external entities is XXE.

Versioning and Inventory

Version prefixes are a security boundary nobody owns. v1 often stays alive for old mobile clients after v2 gains a control, so the test is a differential: send the same request to both prefixes with a token that v2 rejects, and see whether v1 answers. A 401 on v2 and a 200 on v1 is the finding.

Excessive Data Exposure

Compare what an endpoint returns with what the client renders. Serialising the database row and letting the UI hide fields is the common failure: the response then carries password hashes, internal flags or other users' identifiers that no screen shows. The positive is a field the caller is not entitled to — not one you merely find surprising.

API1: Broken Object Level Authorization (BOLA)

BOLA is IDOR applied to APIs: swap an object identifier (integer, UUID, email, or path segment) and see whether the server still authorizes the request. It is the most common and most severe API flaw, but the testing workflow — identifier formats, enumeration, and full CRUD checks — is documented once on the IDOR guide, so this page defers to it rather than restating the technique.

→ IDOR & Object-Level Authorization (canonical guide)

API5: Broken Function Level Authorization (BFLA)

bfla-testing.sh
# Test if regular user can access admin API endpoints:

# Admin endpoints to probe:
curl -s https://target.com/api/admin/users \
  -H 'Authorization: Bearer REGULAR_USER_TOKEN'

curl -s https://target.com/api/admin/config \
  -H 'Authorization: Bearer REGULAR_USER_TOKEN'

# HTTP method tampering:
# If GET is blocked, try other methods:
curl -X PUT https://target.com/api/admin/users/123 \
  -H 'Authorization: Bearer REGULAR_USER_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"role": "admin"}'

# Test admin APIs discovered from documentation:
# POST /api/users/{id}/promote
# DELETE /api/users/{id}
# PUT /api/config/security
# POST /api/reports/generate

# Check if internal/management endpoints are accessible:
curl -s https://target.com/api/internal/metrics
curl -s https://target.com/api/internal/health
curl -s https://target.com/api/debug/vars

JWT Token Attacks

APIs frequently authenticate with JWTs, and a misconfigured verification step (an accepted none algorithm, a weak HMAC secret, or an RS256→HS256 key-confusion bug) turns a token into a full authentication bypass. The decoding, hashcat / jwt_tool cracking, and header-injection techniques are maintained on the dedicated JWT guide.

→ JWT Attacks (canonical guide)

Rate Limiting & Resource Exhaustion

rate-limit-testing.sh
# Test API rate limiting:
for i in $(seq 1 100); do
  CODE=$(curl -s -o /dev/null -w '%{http_code}' https://target.com/api/users)
  echo "Request $i: HTTP $CODE"
done
# If all return 200 → no rate limiting!

# Test resource exhaustion via pagination:
curl -s 'https://target.com/api/users?page=1&per_page=10000' | jq '.data | length'
# Returns all users in one request → memory exhaustion attack

# GraphQL-specific: nested query depth attacks
# Deep nesting can cause exponential server processing
curl -X POST https://target.com/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query": "{users{friends{friends{friends{friends{name}}}}}}"}'

Testing Checklist

  • ☐ 1. Discover and map all API endpoints (Swagger, fuzzing, JS analysis)
  • ☐ 2. Test BOLA on every endpoint with object IDs (change IDs)
  • ☐ 3. Test BFLA — access admin endpoints with regular user tokens
  • ☐ 4. Test mass assignment on all PUT/POST/PATCH endpoints
  • ☐ 5. Analyze JWT tokens for weak algorithms and secrets
  • ☐ 6. Test rate limiting on sensitive endpoints (login, password reset)
  • ☐ 7. Test excessive data exposure — do responses include unnecessary fields?
  • ☐ 8. Test older API versions (/v1/ vs /v2/) for missing security patches
  • ☐ 9. Test HTTP method tampering (GET → PUT, POST → DELETE)
  • ☐ 10. Test content-type manipulation (JSON → XML for XXE)
  • ☐ 11. Read the OPTIONS Allow header, then try every verb it omits and check for a state change
  • ☐ 12. Replay one body as JSON, text/plain and urlencoded — 415 is clean, a created record is not
  • ☐ 13. Diff each version prefix found in discovery against the same token

Evidence Collection

BOLA: Request with user A's token accessing user B's data

BFLA: Regular user accessing admin-only endpoints

JWT: Decoded token, modified token, and server acceptance proof

Verb Matrix: One row per method with status and whether state actually changed — a 200 on OPTIONS is not evidence the handler exists

Content-Type Matrix: The same body sent as JSON, text/plain and urlencoded, with the status and any resource created

Version Diff: The same request against each version prefix with one token, showing where authorisation diverges

CVSS Range: BOLA: 6.5–8.6 | BFLA: 8.1–9.8 | JWT bypass: 8.1–9.8

Remediation

  • • Authorization on every endpoint: Verify the authenticated user owns the requested resource.
  • • Use unpredictable IDs: Use UUIDs instead of sequential integers for resource identifiers.
  • • Response filtering: Only return fields the user is authorized to see.
  • • Rate limiting: Implement per-user and per-IP rate limits with 429 responses.
  • • JWT best practices: Use RS256, short expiry, validate all claims, maintain a deny list for revocation.
  • • Deny unadvertised methods: The router should answer 405 for every verb it does not explicitly route, rather than letting the framework bind whatever the handler accepts.
  • • Pin the parser per route: Reject a body whose Content-Type the route did not declare, with 415, instead of falling back to a lenient binder.
  • • Retire old versions: A version needs a sunset date and the same control set as its successor. Keeping v1 alive for old clients is how the weaker authorisation path survives.

False Positive Identification

  • Verbose error messages by design: Some APIs intentionally return detailed errors for developer experience (especially in development mode) — verify the behavior in the production environment.
  • Missing rate limiting on non-sensitive endpoints: Not every endpoint needs rate limiting — focus on auth, search, and data-export endpoints.
  • API versioning as intended behavior: Accessing /api/v1 when /api/v2 exists isn't a vulnerability unless v1 lacks security controls present in v2.
  • A framework-generated OPTIONS response: Many stacks answer OPTIONS automatically for any path, including ones that do not exist. Check the Allow header against what the documentation claims, and confirm the verb has an effect before calling it exposure.
  • A 200 that changed nothing: Echoing the request body is not the same as persisting it. Re-read the resource (or list it) after the verb test; if the record is unchanged, the handler did not act.

REST API Attack Practice

Test documentation discovery, method abuse, and object-level authorization on API targets.

PortSwigger — API Testing Labs PortSwigger medium
Documentation discoveryHidden endpointsMethod abuse
OWASP crAPI Custom Lab medium
BOLAMass assignmentBroken authentication