Most companies still test their web apps for bugs and skip the APIs feeding data to those apps, mobile clients, and partner integrations. That gap is exactly where attackers go looking. Broken Object Level Authorization (BOLA) alone accounts for a large share of API breaches reported to bug bounty programs, and it barely takes more than swapping an ID number in a request. This tutorial walks through a repeatable, hands-on process for testing API security using the OWASP API Security Top 10 as the checklist, with real commands, real tool configurations, and a working test project you can run today.
By the end you will have a working API test lab, a documented BOLA/IDOR test methodology, automated scans wired into a CI/CD pipeline, and a report template mapped to OWASP categories that you can hand to a development team. Expect to spend around 90 minutes on the initial setup and first full test pass.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why API Security Testing Is Different From Web App Testing
Traditional web application security testing leans heavily on the browser: you click through pages, a proxy captures requests, and a scanner crawls links. APIs do not work that way. There is often no browsable UI at all, just a set of REST endpoints, a GraphQL schema, or a gRPC service definition. Authorization logic sits behind every single call rather than behind a handful of pages, which means the attack surface per endpoint is smaller but the total number of surfaces is much larger.
The OWASP API Security Top 10 was built specifically because generic web vulnerability categories (like the older OWASP Top 10 for web apps) do not map cleanly onto API-specific failure modes. Broken Object Level Authorization, Broken Object Property Level Authorization, and Broken Function Level Authorization are the top three categories, and all three come down to the same root cause: the API trusts an ID or a role claim in the request instead of independently verifying that the caller is allowed to touch that specific resource.
Modern API testing also has to account for shadow and zombie endpoints. Teams ship new API versions constantly, and old versions often stay live long after the documentation stops mentioning them. A tester who only tests what is in the OpenAPI/Swagger file will miss anything that shipped outside that spec, so passive traffic capture becomes just as important as reading documentation.
Prerequisites and Tool Versions
Install these tools before starting. Version numbers below reflect what was current and stable for API security testing work as of September 2026; check each project’s release page if you want the very latest patch.
- Burp Suite Community or Professional (proxy and manual testing) — Community is free and sufficient for this tutorial
- OWASP ZAP 2.16.x (free, open-source alternative or complement to Burp)
- Postman or Insomnia (request collections, environment variables, and scripted test assertions)
- Node.js 22 LTS (to run a small sample API and test scripts)
- Docker 27.x (to run a disposable, intentionally vulnerable API target for practice)
- Python 3.12+ with the
requestslibrary (for scripted authorization tests) - Semgrep CLI (static analysis for API code in CI/CD)
- A GitHub or GitLab account with Actions/CI enabled (for the pipeline integration step)
- Two test user accounts with different roles in your target application (a normal user and an admin, or two separate tenants)
You will also need explicit written authorization to test the target API. Never run these techniques, even the read-only ones, against a system you do not own or do not have a signed engagement letter for. If you do not have a target of your own yet, Step 3 below sets up a deliberately vulnerable sandbox so you can practice safely.
Step 1: Map Your API Attack Surface
Start by building a complete inventory. Do not trust the OpenAPI/Swagger file alone; reconcile it against real traffic. Open Burp Suite, set your browser or mobile device to route through the Burp proxy (default 127.0.0.1:8080), and use the application normally for 15 to 20 minutes, hitting every screen, every filter, every settings page, and every action a logged-in user can take.
Burp’s Target > Site map tab will populate with every endpoint it observes. Export this alongside your OpenAPI spec (if one exists) and diff the two lists. Anything present in live traffic but absent from documentation is a shadow endpoint and goes straight to your priority list, since it usually means nobody has reviewed its authorization logic recently.
# Pull the OpenAPI spec if the app exposes one
curl -s https://api.target.example.com/openapi.json -o openapi.json
# Count documented paths
python3 -c "import json; d=json.load(open('openapi.json')); print(len(d.get('paths', {})))"
# Compare against Burp's exported site map (Target > Site map > right-click > Save selected items)
# then diff the endpoint lists manually or with a small script
If the target is a mobile app, decompile the APK or intercept its traffic with an HTTPS proxy and a trusted certificate installed on a test device. Mobile clients frequently call internal or “admin-only” endpoints that never appear in a public API reference, because the mobile team assumed nobody would read the compiled binary. That assumption breaks the moment someone runs a decompiler.
Step 2: Set Up Two Test Accounts and Baseline Requests
Authorization testing is meaningless with a single account. Create at minimum two accounts: a low-privilege user (User A) and either an admin account or a second tenant’s user (User B). If the application is multi-tenant, User A and User B should belong to different organizations so you can test cross-tenant leakage, not just cross-role leakage.
Log in as each account and capture the authentication token (JWT, session cookie, or API key) in Burp or Postman. Save each as a named environment variable so you can swap between identities with one click during testing rather than re-authenticating every time.
# Postman environment example (Manage Environments > Add)
# variable: user_a_token value: eyJhbGciOi...
# variable: user_b_token value: eyJhbGciOi...
# Quick manual baseline check with curl
curl -s -H "Authorization: Bearer $USER_A_TOKEN" \
https://api.target.example.com/v1/orders/1042 | jq .
curl -s -H "Authorization: Bearer $USER_B_TOKEN" \
https://api.target.example.com/v1/orders/1042 | jq .
# If User B's request returns User A's order data (HTTP 200 instead of 403/404),
# you have found a BOLA vulnerability.
Record the expected response for each account against each endpoint before you start mutating requests. Without a documented baseline, it is easy to misjudge whether a 200 response with an empty body counts as a pass or a partial data leak.
Step 3: Stand Up a Safe Practice Target With Docker
If you do not have written authorization to test a production or staging API yet, practice on a disposable, intentionally vulnerable target instead. OWASP’s crAPI (Completely Ridiculous API) project is built for exactly this purpose and mirrors many of the flaws found in real production APIs, including BOLA, excessive data exposure, and mass assignment.
git clone https://github.com/OWASP/crAPI.git
cd crAPI
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d
# crAPI web UI will be available at http://localhost:8888
# API gateway at http://localhost:8888/identity, /community, /workshop
Give the containers a minute or two to initialize, then register two accounts through the web UI to use as your User A and User B for every exercise in this tutorial. Everything below can be practiced safely against this local instance before you ever point these techniques at a real target.
Step 4: Test Broken Object Level Authorization (BOLA)
BOLA (API1 in the OWASP API Security Top 10) is the single most common and most damaging API flaw because it is trivial to exploit and often trivial to introduce. It happens when an endpoint accepts an object ID from the client and returns or modifies that object without checking whether the authenticated caller actually owns or has permission to access it.
Test every endpoint that accepts an ID in the URL path, query string, or request body. Authenticate as User A, note an object ID that belongs to User A, then repeat the exact same request while authenticated as User B (or vice versa). A secure API returns 403 Forbidden or 404 Not Found. A vulnerable API returns 200 OK with someone else’s data.
import requests
BASE = "http://localhost:8888/workshop/api/shop/orders"
user_a_token = "..."
user_b_token = "..."
# User A creates or already has order ID 5
resp = requests.get(f"{BASE}/5", headers={"Authorization": f"Bearer {user_b_token}"})
print(resp.status_code, resp.text[:200])
# Expected (secure): 403 or 404
# Vulnerable result: 200 with User A's order details visible to User B
Automate this across every numeric or UUID-based ID you found during mapping in Step 1. A simple loop that increments or decrements IDs by small amounts (object ID minus 1, plus 1, plus 10) will often surface neighboring records that belong to other users, which is useful for scoping the severity of a finding beyond a single record.
Step 5: Test Broken Function Level Authorization (BFLA)
BFLA (API5) is the functional cousin of BOLA. Instead of accessing someone else’s data, the attacker calls a function they should not be allowed to call at all, such as an admin-only endpoint, a bulk delete action, or a password reset override that a regular user account should never reach.
Capture every admin-facing request while logged in with an admin account, then replay each one using the low-privilege User A token. Also test HTTP method substitution: if GET /v1/users/42 is properly restricted, check whether PUT, PATCH, or DELETE on the same path enforce the same rule, since authorization checks are sometimes implemented per-route rather than per-resource and get missed on less common verbs.
# Captured as admin: DELETE /v1/users/42 -> 200 OK
# Replay as regular user
curl -s -X DELETE -H "Authorization: Bearer $USER_A_TOKEN" \
-w "\nHTTP status: %{http_code}\n" \
https://api.target.example.com/v1/users/42
# Also test verb tampering on a route that only restricts GET
curl -s -X PATCH -H "Authorization: Bearer $USER_A_TOKEN" \
-H "Content-Type: application/json" -d '{"role":"admin"}' \
-w "\nHTTP status: %{http_code}\n" \
https://api.target.example.com/v1/users/self
Pay close attention to any endpoint that lets a user modify their own profile. Mass assignment bugs frequently hide here: if the API accepts a full JSON object and blindly updates every field present, a user can sometimes add a "role": "admin" or "isVerified": true field to their own update request and have it silently accepted.
Step 6: Check for Excessive Data Exposure and Mass Assignment
APIs frequently return more data than the client interface displays, on the assumption that the frontend will just filter it out. That assumption fails the moment someone inspects the raw response instead of the rendered page. Capture every response body during normal use and grep for fields the UI never shows: internal flags, other users’ emails, pricing cost data, or full address records when only a city was displayed.
# Pull a raw API response and inspect every field, not just what the UI renders
curl -s -H "Authorization: Bearer $USER_A_TOKEN" \
https://api.target.example.com/v1/profile/self | python3 -m json.tool
# Look specifically for: password_hash, internal_notes, ssn, cost_price,
# other_user_id references, admin_flags, or full PII beyond what's displayed
For mass assignment, take a legitimate update request and add extra fields one at a time that you would not expect to be user-editable, such as role, accountBalance, isAdmin, or tier. If the modified field shows up in a follow-up GET request, the API accepted an update it should have rejected, and you have a mass assignment vulnerability (API3 in the current OWASP API Top 10).
Step 7: Test Rate Limiting and Resource Consumption
Unrestricted resource consumption (API4) covers everything from brute-forceable login endpoints to pagination parameters that let a client request an absurd number of records in one call. Both are cheap to test and commonly overlooked because they do not cause an obvious error during normal QA.
Test authentication endpoints first, since they are the highest-value rate-limiting target. Send a rapid sequence of failed login attempts and confirm the API throttles, locks, or CAPTCHAs the account or IP after a reasonable threshold (most guidance points to somewhere between 5 and 10 attempts before a delay or lockout kicks in).
# Simple rate-limit probe against a login endpoint
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code} " \
-X POST -H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"wrong-password"}' \
https://api.target.example.com/v1/auth/login
done
echo
# Expect 429 (Too Many Requests) or account lockout messaging after a small number of attempts.
# If every attempt returns 200/401 with no throttling, flag it as an API4 finding.
For pagination abuse, try requesting an unusually large page size, such as ?limit=1000000 or ?page_size=999999, on any list endpoint. A properly hardened API caps the maximum page size server-side regardless of what the client requests; a vulnerable one will attempt to serialize the entire table and either time out, exhaust memory, or dump far more records than intended.
Step 8: Fuzz Inputs for Injection and SSRF
Injection flaws (SQL, NoSQL, command injection) and Server-Side Request Forgery do not disappear just because the client is a JSON API instead of an HTML form. Every parameter that reaches a query, a shell command, or an outbound HTTP call on the server side is a candidate, including headers, path segments, and nested JSON body fields, not just obvious query string parameters.
Burp’s Intruder tool automates this well: mark every parameter as a fuzzing position and load a payload list covering SQL injection strings, NoSQL operators like $ne or $gt, and common SSRF probes such as internal IP ranges or cloud metadata endpoints (169.254.169.254).
# NoSQL injection probe against a MongoDB-backed login endpoint
curl -s -X POST -H "Content-Type: application/json" \
-d '{"email":{"$ne":null},"password":{"$ne":null}}' \
https://api.target.example.com/v1/auth/login
# SSRF probe: any field that accepts a URL (webhooks, avatar-by-URL, PDF export)
curl -s -X POST -H "Authorization: Bearer $USER_A_TOKEN" \
-H "Content-Type: application/json" \
-d '{"webhook_url":"http://169.254.169.254/latest/meta-data/"}' \
https://api.target.example.com/v1/integrations/webhook
Any parameter that accepts a URL from the client, such as webhook configuration, avatar uploads by URL, or PDF/image rendering services that fetch a remote resource, deserves dedicated SSRF testing. These features are convenient and increasingly common in SaaS products, and they are one of the fastest-growing API-specific attack categories because developers rarely think of “fetch this URL for the user” as a security-sensitive operation.
Step 9: Test GraphQL-Specific Risks
GraphQL APIs introduce their own risk categories on top of the standard REST issues above. Introspection, if left enabled in production, hands an attacker the entire schema, including field names, types, and mutations that were never meant to be public knowledge.
# Check whether introspection is enabled (it should be disabled in production)
curl -s -X POST -H "Content-Type: application/json" \
-d '{"query":"{__schema{types{name}}}"}' \
https://api.target.example.com/graphql | jq '.data.__schema.types | length'
# If this returns a non-zero count in production, introspection is exposed.
Also test for deeply nested queries and query batching abuse, both of which can be used to force the server to do disproportionate work from a single, small request. A query that nests a relationship five or six levels deep (users → posts → comments → author → posts → comments) can multiply database load exponentially if the server does not enforce query depth or complexity limits. Tools like graphql-cop automate a first pass across most of these checks.
Step 10: Automate Checks With OWASP ZAP
Manual testing catches logic flaws that scanners miss, but automation catches everything you would otherwise forget to check consistently across dozens of endpoints. OWASP ZAP can import an OpenAPI spec directly and run an authenticated active scan against every documented path.
# ZAP CLI: import OpenAPI spec and run a baseline scan
docker run -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable zap-api-scan.py \
-t https://api.target.example.com/openapi.json \
-f openapi \
-r zap-report.html \
-z "-config replacer.full_list(0).description=auth \
-config replacer.full_list(0).enabled=true \
-config replacer.full_list(0).matchtype=REQ_HEADER \
-config replacer.full_list(0).matchstr=Authorization \
-config replacer.full_list(0).replacement='Bearer YOUR_TOKEN_HERE'"
The generated HTML report flags common issues like missing security headers, verbose error messages that leak stack traces, and TLS misconfigurations. Treat this as a floor, not a ceiling: ZAP will not find a BOLA flaw because it has no concept of “this record belongs to a different user,” which is exactly why Steps 4 through 6 above have to stay manual or custom-scripted.
Step 11: Wire API Security Testing Into CI/CD
One-off manual testing goes stale the moment a developer ships a new endpoint. The current best practice, reflected in 2026 guidance from multiple API security vendors, is a layered pipeline: static analysis (SAST) and dependency scanning (SCA) on every pull request, authenticated dynamic scanning (DAST) against a staging environment on a schedule, and a full manual or PTaaS-style pentest after major architecture changes or roughly annually.
# .github/workflows/api-security.yml
name: API Security Checks
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
run: |
pip install semgrep
semgrep --config p/owasp-top-ten --error --json -o semgrep-results.json .
- name: Fail on high-severity findings
run: |
python3 -c "
import json, sys
data = json.load(open('semgrep-results.json'))
high = [r for r in data['results'] if r.get('extra',{}).get('severity') == 'ERROR']
print(f'{len(high)} high-severity findings')
sys.exit(1 if high else 0)
"
Set the pipeline to block merges on critical findings only, and route medium and low findings to a backlog rather than blocking every PR. A security gate that developers cannot get past for a minor finding trains the team to bypass or ignore the tool entirely, which defeats the purpose of automating in the first place.
Step 12: Document Findings and Map to OWASP Categories
A finding without a reproducible request and a clear owner rarely gets fixed. For every issue you confirm, capture the exact request (method, URL, headers, body), the response that proves the issue, the OWASP API Security Top 10 category it maps to, a severity rating, and the specific team or repository that owns the affected code.
| Finding | OWASP Category | Severity | Reproduction | Owner |
|---|---|---|---|---|
| User B can view User A’s orders via /orders/{id} | API1 – BOLA | Critical | GET request with swapped ID, auth as User B | Orders service team |
| Regular user can DELETE /users/{id} | API5 – BFLA | Critical | DELETE replay with low-priv token | Identity service team |
| Profile update accepts role field | API3 – Mass Assignment | High | PATCH with extra “role”:”admin” field | Profile service team |
| Login endpoint has no rate limit | API4 – Resource Consumption | Medium | 15 rapid failed logins, no 429 | Auth team |
| GraphQL introspection enabled in prod | API9 – Improper Inventory | Medium | __schema query returns full type list | Platform team |
This mapping does double duty: it gives engineering a consistent taxonomy to track recurring issue types over time, and it gives leadership a way to compare your API’s risk posture against industry-standard categories rather than an ad hoc list of bugs. Track mean time to remediate (MTTR) per category as a metric, since a team that consistently takes months to fix BOLA findings has a process problem, not just a code problem.
Common Pitfalls in API Security Testing
Even experienced testers fall into predictable traps when moving from web app testing to API-focused testing. Watch for these specifically.
- Trusting the OpenAPI spec as complete. Documentation drifts from reality within weeks. Always reconcile against captured live traffic, as covered in Step 1.
- Testing with only one account. Single-account testing cannot surface BOLA, BFLA, or cross-tenant leakage by definition, since those bugs only appear when comparing what two different identities can access.
- Ignoring HTTP verbs other than GET and POST. Authorization checks are frequently applied per-route instead of per-resource, so PUT, PATCH, and DELETE on an otherwise-protected path can slip through unchecked.
- Assuming client-side filtering equals server-side security. A field the UI hides is not a field the API withholds. Always inspect raw response bodies.
- Running automated scanners without authentication. An unauthenticated DAST scan against an API that requires a token will report a false “no vulnerabilities found,” because nearly every interesting endpoint returned 401 before the scanner ever reached the actual logic.
- Skipping mobile and internal APIs. Public-facing web APIs get most of the attention while the mobile-only or internal microservice APIs, often carrying identical or worse authorization bugs, go untested.
- Setting an all-or-nothing CI gate. Blocking every PR on any finding, including low-severity noise, trains developers to route around the security tooling entirely.
Troubleshooting Common Issues
Here are the problems testers run into most often during setup and execution, along with the fix for each.
- Burp Suite shows no traffic despite proxy configuration. Confirm the device’s proxy settings point to the correct IP and port, and that Burp’s CA certificate is installed and trusted on the test device, not just downloaded.
- crAPI containers fail to start or exit immediately. Check Docker has at least 4GB of memory allocated; crAPI runs multiple microservices plus a database and message queue, and under-provisioned Docker Desktop will silently kill containers under memory pressure.
- API returns 401 for every request, even with a valid-looking token. Tokens frequently expire in 15 to 60 minutes. Re-authenticate and confirm you are sending the header in the exact format the API expects (some require “Bearer” prefix, others do not).
- ZAP active scan produces almost no findings. Verify the authentication replacer rule actually fired by checking the ZAP request log; a misconfigured token replacement means every request was sent unauthenticated and rejected before reaching real logic.
- GraphQL introspection query returns an error instead of the schema. This usually means introspection is properly disabled, which is the desired secure state, not a testing failure.
- Rate-limit test shows inconsistent results. Load balancers with multiple backend instances sometimes apply rate limits per-instance rather than globally, so requests appear to succeed longer than expected. Test from a single source IP over a sustained period to get an accurate read.
- Semgrep CI step passes locally but fails in the pipeline. Confirm the CI runner is scanning the same file paths and using the same ruleset version; ruleset updates between local and CI runs can change results.
- Mass assignment test shows the extra field was “accepted” but has no effect. Some frameworks accept unknown fields silently without persisting them. Always confirm with a follow-up GET request that the value actually changed in the datastore, not just that the POST/PATCH returned 200.
- Postman environment variables not resolving in requests. Confirm the correct environment is selected in the top-right dropdown; a saved variable in the wrong environment scope will silently fail to substitute.
Sample Output: What a Finished Test Report Looks Like
A well-structured API security test report should read clearly to both engineers and non-technical stakeholders. Below is a condensed example of what the executive summary section might look like after completing the steps in this tutorial against a sample e-commerce API.
API Security Test Summary - Orders & Identity Services
Test window: Sept 1-5, 2026
Endpoints inventoried: 47 (42 documented, 5 shadow endpoints found via traffic capture)
Test accounts used: 2 (standard user, admin) across 2 tenant organizations
Findings by severity:
Critical: 2 (BOLA on /orders/{id}, BFLA on DELETE /users/{id})
High: 3 (mass assignment, excessive data exposure x2)
Medium: 4 (missing rate limits, GraphQL introspection enabled,
verbose error stack traces, missing pagination cap)
Low: 6 (missing security headers, minor info disclosure)
Overall risk posture: HIGH - two critical findings allow cross-account
data access and unauthorized account deletion. Recommend hotfix within
48 hours for both critical items before next findings review cycle.
Notice that the summary leads with business impact (cross-account data access, unauthorized deletion) rather than technical jargon. Whoever reads this report first is often a manager deciding whether to delay a release, not the engineer who will fix the code, so the framing needs to work for both audiences.
Advanced Tips for Ongoing API Security Programs
Once the one-time testing pass above is complete, a handful of practices turn this into a sustainable program rather than a periodic fire drill.
Adopt schema validation at the API gateway layer so that any request or response that does not match the declared OpenAPI or GraphQL schema gets rejected or flagged automatically. This catches a large share of excessive data exposure and mass assignment issues before a human ever has to test for them, because the gateway enforces the contract rather than relying on every backend service to do it consistently.
Track API drift over time. New endpoints, new fields, and new integrations appear constantly in any actively developed product, and a testing program that only runs once a quarter will always be behind. Feeding your traffic capture and OpenAPI diffing process (from Step 1) into a scheduled weekly job, rather than a one-time manual pass, keeps your inventory current without requiring a full manual retest every time.
Build a library of reusable authorization test scripts per resource type rather than per endpoint. Most APIs have a handful of resource patterns (owned-by-user, owned-by-tenant, publicly readable, admin-only) repeated across dozens of endpoints. A parameterized test script that takes an endpoint path and expected ownership model as input can be pointed at every new endpoint in minutes instead of writing a bespoke test each time.
Finally, treat business logic abuse testing (API6 in the current OWASP list) as a separate, scheduled exercise rather than folding it into routine scans. Automated tools cannot reason about whether a discount code can be applied twice, whether a checkout flow can be replayed to duplicate an order, or whether a referral bonus can be farmed by creating fake accounts. These require a human who understands the specific business rules of the product, and they tend to surface the most expensive fraud losses when left untested.
Complete Working Project: A Minimal API Security Test Harness
Below is a compact, complete Python test harness that runs the core authorization checks from Steps 4 through 7 against any REST API. Save it as api_security_harness.py, fill in your two tokens and a list of endpoint/ID pairs, and run it as a first-pass automated check before or alongside manual testing.
import requests
import time
BASE_URL = "https://api.target.example.com"
USER_A_TOKEN = "REPLACE_ME"
USER_B_TOKEN = "REPLACE_ME"
# List resources User A owns, to test if User B can access them
RESOURCES_TO_TEST = [
{"path": "/v1/orders/{id}", "id": "1042", "methods": ["GET", "PATCH", "DELETE"]},
{"path": "/v1/profile/{id}", "id": "88", "methods": ["GET", "PATCH"]},
{"path": "/v1/invoices/{id}", "id": "301", "methods": ["GET"]},
]
def headers_for(token):
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
def test_bola(resource):
url = BASE_URL + resource["path"].format(id=resource["id"])
results = []
for method in resource["methods"]:
resp = requests.request(method, url, headers=headers_for(USER_B_TOKEN), timeout=10)
vulnerable = resp.status_code == 200
results.append({
"url": url,
"method": method,
"status": resp.status_code,
"vulnerable": vulnerable,
})
time.sleep(0.5) # be gentle with the target
return results
def test_rate_limit(login_path, attempts=12):
url = BASE_URL + login_path
codes = []
for _ in range(attempts):
resp = requests.post(url, json={"email": "[email protected]", "password": "wrong"}, timeout=10)
codes.append(resp.status_code)
throttled = 429 in codes or codes.count(401) < attempts
return {"codes": codes, "throttled_or_locked": throttled}
if __name__ == "__main__":
print("=== BOLA / BFLA Cross-Account Tests ===")
critical_findings = 0
for resource in RESOURCES_TO_TEST:
for result in test_bola(resource):
flag = "CRITICAL FINDING" if result["vulnerable"] else "ok"
if result["vulnerable"]:
critical_findings += 1
print(f'[{flag}] {result["method"]} {result["url"]} -> {result["status"]}')
print("\n=== Rate Limit Test: /v1/auth/login ===")
rl_result = test_rate_limit("/v1/auth/login")
print(f'Response codes: {rl_result["codes"]}')
print(f'Rate limiting detected: {rl_result["throttled_or_locked"]}')
print(f"\nTotal critical BOLA/BFLA findings: {critical_findings}")
Run it with python3 api_security_harness.py after filling in real tokens and endpoint IDs from your own environment. Extend the RESOURCES_TO_TEST list as you discover more endpoints during the mapping phase in Step 1, and consider adding this script (or a version of it) as a scheduled job against staging so that a regression in authorization logic gets caught within a day instead of at the next quarterly pentest.
Comparing API Security Testing Tools
No single tool covers every category in the OWASP API Security Top 10. The table below summarizes where each common tool fits so you can decide what belongs in your stack.
| Tool | Best For | Cost | Catches BOLA/BFLA? |
|---|---|---|---|
| Burp Suite Professional | Manual testing, Intruder fuzzing | Paid license (Community free, limited) | With manual scripting only |
| OWASP ZAP | Automated baseline + active scans | Free, open-source | No (logic-blind) |
| Postman/Insomnia + scripts | Scripted authorization test suites | Free tier available | Yes, with custom test scripts |
| Semgrep | Static analysis in CI/CD | Free CLI, paid platform tiers | Partial (pattern-based only) |
| Custom Python harness (this tutorial) | Repeatable cross-account authorization checks | Free (self-built) | Yes, by design |
The practical takeaway: automated scanners are necessary but not sufficient. BOLA and BFLA, the two most damaging categories in the current OWASP API Security Top 10, require a tool (or a custom script, as built in this tutorial) that actually understands who owns what, which is a business-logic concept no generic scanner can infer on its own.
Building a Recurring Testing Cadence
A single test pass, however thorough, only reflects the API’s state on the day you ran it. Establish a cadence that matches how often the API actually changes. A reasonable baseline for a mid-sized engineering team: SAST and SCA scans on every pull request touching API code, an authenticated DAST scan against staging weekly, the custom authorization harness from this tutorial run against staging after every deploy, and a full manual review, ideally by someone outside the team that built the feature, after any major architectural change or roughly every 6 to 12 months.
Assign an owner for the testing program itself, not just for individual findings. Without a named owner, testing cadences tend to quietly lapse the first time the team gets busy with a product deadline, and the gap between “we tested this once” and “we test this continuously” is exactly where most of the API breaches reported in 2025 and 2026 originated.
Frequently Asked Questions
What is the difference between API security testing and API penetration testing?
API security testing is the broader, ongoing practice covering automated scanning, code review, and manual checks integrated into development. API penetration testing is typically a scoped, time-boxed engagement, often performed by an external team, that simulates a real attacker against a defined target within an agreed window.
Do I need Burp Suite Professional, or is the free Community edition enough?
The free Community edition covers everything in this tutorial: proxying traffic, manual request replay, and basic site mapping. Burp Professional adds the automated active scanner and unlimited Intruder speed, which becomes more valuable once you are testing large numbers of endpoints regularly rather than doing occasional manual checks.
How often should BOLA and BFLA testing be repeated?
Ideally on every pull request that touches authorization logic, and at minimum after every deploy to staging. These are the categories most likely to be reintroduced by a routine code change, since a developer adding a new field or endpoint can easily forget to copy the ownership check from an existing, similar endpoint.
Is it legal to test an API for BOLA and BFLA vulnerabilities?
Only with explicit written authorization from the API owner, whether that is your own employer’s staging environment, a signed penetration testing engagement, or a public bug bounty program’s defined scope. Testing production systems you do not own or have permission to test can violate computer fraud laws even when the intent is purely educational.
Can automated scanners like ZAP replace manual API testing entirely?
No. Scanners are effective at finding missing security headers, known CVEs in dependencies, and basic injection flaws, but they have no concept of which user is supposed to own which resource, so they will not reliably find BOLA, BFLA, or business logic abuse without significant custom configuration.
What is the fastest way to find shadow API endpoints?
Capture live traffic with a proxy like Burp or ZAP while using every feature of the application (including mobile clients), then diff the captured endpoint list against the official OpenAPI or GraphQL schema. Anything present in traffic but absent from documentation is a shadow endpoint.
Should GraphQL introspection always be disabled in production?
Yes, for public-facing production environments. Introspection is useful during development and for internal tooling but hands an external attacker a complete map of your schema, including fields and mutations that were never intended to be discoverable.
How do I convince a development team to prioritize fixing BOLA findings?
Frame the finding in terms of concrete business impact using the report format in this tutorial: show exactly what data or action an unauthorized user could access, attach the reproducible request, and map it to a named OWASP category so the team can see it is a recognized, high-severity class of bug rather than an isolated edge case.


