API Security Testing Scope
API security testing is not the same as web application security testing. APIs expose business logic more directly, often return richer data, and are attacked by automated scripts rather than humans in browsers.
The OWASP API Security Top 10 defines the most critical API vulnerabilities:
| Rank | Vulnerability | Example |
|---|---|---|
| API1 | Broken Object Level Authorization (BOLA) | User A accesses User B's data via /api/orders/456 |
| API2 | Broken Authentication | Weak tokens, missing expiry, no brute-force protection |
| API3 | Broken Object Property Level Auth | Mass assignment, exposing internal fields |
| API4 | Unrestricted Resource Consumption | No rate limiting, large payloads accepted |
| API5 | Broken Function Level Authorization | Regular users can call admin endpoints |
| API6 | Unrestricted Access to Sensitive Business Flows | Bot-accessible purchase flows |
| API7 | Server-Side Request Forgery (SSRF) | API fetches attacker-controlled URLs |
| API8 | Security Misconfiguration | Debug endpoints exposed, verbose errors |
| API9 | Improper Inventory Management | Undocumented endpoints left accessible |
| API10 | Unsafe Consumption of APIs | Trusting upstream APIs without validation |
OWASP ZAP
OWASP ZAP (Zed Attack Proxy) is a free, open-source security scanner that works well for automated API scanning in CI pipelines.
API Scan via Docker
# API scan using OpenAPI spec
docker run --rm \
-v $(pwd):/zap/wrk \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t http://host.docker.internal:8000/openapi.yaml \
-f openapi \
-r zap-report.html \
-J zap-report.json
ZAP in CI (GitHub Actions)
- name: Start API
run: docker compose up -d api
- name: ZAP API Scan
uses: zaproxy/[email protected]
with:
target: http://localhost:8000/openapi.yaml
format: openapi
fail_action: true
rules_file_name: .zap/rules.tsv # suppress known false positives
cmd_options: '-a' # include alpha active scan rules
- name: Upload ZAP report
uses: actions/upload-artifact@v4
if: always()
with:
name: zap-report
path: report_html.html
Suppressing False Positives
Create .zap/rules.tsv to suppress rules that don't apply:
# Rule ID Threshold Comment
10096 OFF Timestamp disclosure is acceptable for our API
90005 OFF CSP not applicable (JSON API, no HTML)
Burp Suite
Burp Suite (Community or Professional) is the industry-standard tool for manual API security testing. It acts as an intercepting proxy between your test client and the API.
Setup
- Configure your HTTP client to proxy through Burp (default:
127.0.0.1:8080) - Import your OpenAPI spec via Target > Import > From URL
- Browse or script API calls — all traffic appears in Proxy > HTTP history
Repeater for Manual Testing
# Capture this request in Burp Proxy, send to Repeater:
GET /api/orders/1234 HTTP/1.1
Host: api.example.com
Authorization: Bearer user-a-token
# Modify the order ID to another user's order:
GET /api/orders/5678 HTTP/1.1 ← User B's order
Host: api.example.com
Authorization: Bearer user-a-token ← Still User A's token
# If you get 200, you have a BOLA vulnerability.
# Expected: 403 Forbidden
Intruder for Fuzzing
Burp Intruder automates fuzzing attack payloads. For API testing:
POST /api/login HTTP/1.1
Content-Type: application/json
{"email": "§[email protected]§", "password": "§password§"}
Set payload positions around § markers. Intruder will try each payload and show which ones return 200 (success) vs 401 (failure), revealing weak credentials.
Testing BOLA (Broken Object Level Authorization)
BOLA is the most common API vulnerability. Write explicit tests:
import pytest
from django.test import TestCase
from myapp.models import User, Order
class BOLATests(TestCase):
def setUp(self):
self.user_a = User.objects.create_user('alice', password='pass')
self.user_b = User.objects.create_user('bob', password='pass')
self.order_b = Order.objects.create(user=self.user_b, total=9999)
def test_user_a_cannot_access_user_b_order(self):
self.client.force_login(self.user_a)
response = self.client.get(f'/api/orders/{self.order_b.id}/')
# Must return 403 or 404 — NOT 200
self.assertIn(response.status_code, [403, 404])
def test_user_a_cannot_delete_user_b_order(self):
self.client.force_login(self.user_a)
response = self.client.delete(f'/api/orders/{self.order_b.id}/')
self.assertIn(response.status_code, [403, 404])
self.assertTrue(Order.objects.filter(pk=self.order_b.pk).exists())
Testing Mass Assignment
def test_user_cannot_set_admin_flag_via_api():
self.client.force_login(self.regular_user)
response = self.client.patch('/api/profile/', {
'name': 'Hacker',
'is_admin': True, # should be ignored
'credits': 99999, # should be ignored
}, content_type='application/json')
# Request might succeed (200) but the sensitive fields must not change
assert response.status_code in (200, 400)
self.regular_user.refresh_from_db()
assert self.regular_user.is_admin is False
assert self.regular_user.credits == 0 # unchanged
Automation Pipeline
A practical API security pipeline:
# Layer 1: Pre-commit (fast, developer machine)
# - bandit (Python SAST)
# - semgrep (pattern matching for common vulnerabilities)
# Layer 2: CI (each PR)
# - ZAP API scan against staging
# - Explicit BOLA/auth tests in test suite
# Layer 3: Scheduled (weekly/nightly)
# - Full Burp Suite scan (if Pro)
# - Dependency audit (pip-audit, npm audit)
steps:
- name: SAST
run: |
pip install bandit semgrep
bandit -r myapp/ -ll
semgrep --config=p/python --config=p/owasp-top-ten .
- name: Dependency audit
run: pip-audit --require-hashes -r requirements.txt
Security testing is not a one-time audit — it should be woven into every PR and every deployment cycle. Automated scanning catches the low-hanging fruit; manual testing and explicit authorization tests catch the vulnerabilities that matter most.