DAST for REST APIs: Automated Security Testing with OWASP ZAP and Nuclei
Dynamic Application Security Testing (DAST) attacks your running API to find vulnerabilities that static analysis misses: authentication bypass, BOLA/IDOR (Broken Object-Level Authorization), injection through actual HTTP requests, and security misconfigurations. This guide covers OWASP ZAP API Scan and Nuclei for REST API DAST in CI/CD pipelines.
Why REST APIs Need DAST
SAST reads your code. DAST runs against your live API — sending actual HTTP requests with malicious payloads, observing responses, and identifying vulnerabilities that only manifest at runtime.
What DAST finds that SAST misses:
- Authentication bypass (JWT algorithm confusion, session fixation)
- Broken Object-Level Authorization — user A accessing user B's data
- Rate limiting missing on sensitive endpoints
- Server-side request forgery (SSRF) via URL parameters
- Security headers missing (CORS misconfiguration, missing CSP)
- Verbose error messages exposing stack traces
- HTTP methods not properly restricted
What DAST cannot find: Logic bugs, business rule violations, multi-step flows that require human understanding. DAST finds infrastructure and protocol-level issues; functional test coverage requires explicit test cases.
Tool 1: OWASP ZAP API Scan
OWASP ZAP's api-scan.py is a Docker-based scanner that takes an OpenAPI spec and attacks every endpoint automatically.
Prerequisites: Your API must be running and accessible. ZAP will crawl via the OpenAPI spec and attack discovered endpoints.
Run against a running API:
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t http://host.docker.internal:8080/api/openapi.json \
-f openapi \
-r zap-report.html \
-J zap-report.json \
-z "-config scanner.threadPerHost=5"host.docker.internal resolves to the Docker host — works for scanning localhost APIs from inside a container.
Scan a remote staging environment:
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t https://api.staging.myapp.com/openapi.json \
-f openapi \
-r zap-report.html \
-J zap-report.json \
--fail-on warnAdd authentication (Bearer token):
# Create auth config file
cat > zap-auth.conf << EOF
replacer.full_list(0).description=auth-header
replacer.full_list(0).enabled=true
replacer.full_list(0).matchtype=REQ_HEADER
replacer.full_list(0).matchstr=Authorization
replacer.full_list(0).regex=false
replacer.full_list(0).replacement=Bearer eyJhbGci...your-jwt-token
EOF
docker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t https://api.staging.myapp.com/openapi.json \
-f openapi \
-z "-configfile /zap/wrk/zap-auth.conf" \
-r zap-report.htmlSuppress false positives with rules file (zap-rules.conf):
# Suppress "X-Content-Type-Options header missing" globally
10021 IGNORE
# Suppress "CSP header not set" for specific URL
10038 IGNORE url:https://api.staging.myapp.com/healthdocker run -v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t https://api.staging.myapp.com/openapi.json \
-f openapi \
-c /zap/wrk/zap-rules.conf \
-r zap-report.htmlGitHub Actions integration:
name: DAST API Scan
on:
push:
branches: [main]
jobs:
dast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start API (Docker Compose)
run: docker compose up -d api
- name: Wait for API
run: |
timeout 60 bash -c 'until curl -sf http://localhost:8080/health; do sleep 2; done'
- name: ZAP API Scan
uses: zaproxy/action-api-scan@v0.9.0
with:
target: 'http://localhost:8080/api/openapi.json'
format: openapi
fail_action: true
cmd_options: '-J zap-report.json'
rules_file_name: 'zap-rules.conf'
- name: Upload ZAP Report
if: always()
uses: actions/upload-artifact@v4
with:
name: zap-api-report
path: zap-report.jsonWhat ZAP API Scan checks:
| Check | OWASP Category |
|---|---|
| SQL Injection | A03:2021 |
| XSS in JSON responses | A03:2021 |
| Path traversal | A01:2021 |
| Missing security headers | A05:2021 |
| Server information disclosure | A05:2021 |
| CORS misconfiguration | A07:2021 |
| Insecure HTTP methods (PUT/DELETE without auth) | A01:2021 |
| JWT algorithm confusion | A02:2021 |
Tool 2: Nuclei for API Security
Nuclei is a template-based scanner with 9,000+ community-maintained templates targeting CVEs, misconfigurations, and API-specific vulnerabilities. It's faster than ZAP and easily extensible.
Install:
# macOS
brew install nuclei
# Linux
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Update templates
nuclei -update-templatesBasic API scan:
nuclei -target https://api.staging.myapp.com \
-tags api,auth,token,jwt \
-severity medium,high,critical \
-json-export nuclei-report.jsonAPI-specific template categories:
# JWT vulnerabilities
nuclei -target https://api.staging.myapp.com \
-tags jwt \
-j nuclei-jwt.json
# Authentication flaws
nuclei -target https://api.staging.myapp.com \
-tags auth-bypass \
-j nuclei-auth.json
# Security header checks
nuclei -target https://api.staging.myapp.com \
-tags headers,misconfig \
-j nuclei-headers.jsonCustom Nuclei template for BOLA testing (checking if a user can access another user's resource):
# templates/bola-test.yaml
id: bola-order-endpoint
info:
name: BOLA - Order ID Enumeration
severity: high
description: Tests if order endpoint enforces object-level authorization
http:
- method: GET
path:
- "{{BaseURL}}/api/v1/orders/{{id}}"
attack: sniper
payloads:
id:
- "1"
- "2"
- "999"
- "1000"
headers:
Authorization: "Bearer {{token}}"
matchers-condition: and
matchers:
- type: status
status: [200]
- type: word
words:
- '"userId"'
condition: and
extractors:
- type: regex
name: user_id
part: body
regex:
- '"userId":\s*"([^"]+)"'Run it:
nuclei -target https://api.staging.myapp.com \
-t templates/bola-test.yaml \
-var token=$API_TOKEN \
-j nuclei-bola.jsonGitHub Actions with Nuclei:
- name: Nuclei API Security Scan
uses: projectdiscovery/nuclei-action@main
with:
target: https://api.staging.myapp.com
flags: "-tags api,jwt,auth -severity medium,high,critical"
github-report: true
github-token: ${{ secrets.GITHUB_TOKEN }}Testing OWASP API Security Top 10
The OWASP API Security Top 10 defines the most critical API risks. Here's how to test each:
API1: Broken Object-Level Authorization (BOLA/IDOR)
# Login as user A, get their order ID
# Attempt to access with user B's token
curl -H "Authorization: Bearer $USER_B_TOKEN" \
https://api.staging.myapp.com/api/v1/orders/$USER_A_ORDER_ID
# Expected: 403 Forbidden
# Vulnerable: 200 OK with user A's dataAPI2: Broken Authentication
# Test JWT algorithm confusion (none algorithm)
# Decode token, change alg to "none", remove signature
python3 -c "
import base64, json
header = json.dumps({'alg': 'none', 'typ': 'JWT'}).encode()
payload = json.dumps({'sub': '1', 'role': 'admin'}).encode()
token = base64.urlsafe_b64encode(header).rstrip(b'=') + b'.' + \
base64.urlsafe_b64encode(payload).rstrip(b'=') + b'.'
print(token.decode())
"
# Try the unsigned token — should get 401, not dataAPI3: Broken Object Property-Level Authorization (Mass Assignment)
# Send extra fields that should not be settable by users
curl -X PATCH https://api.staging.myapp.com/api/v1/users/me \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "John", "role": "admin", "isActive": true}'
# Check response: did role change in subsequent GET /me?API5: Broken Function-Level Authorization
# Test admin endpoints without admin token
curl -X DELETE https://api.staging.myapp.com/api/v1/admin/users/123 \
-H "Authorization: Bearer $REGULAR_USER_TOKEN"
# Expected: 403 ForbiddenIntegrating DAST into CI Without Slowing Down
DAST is slower than SAST. A ZAP scan can take 5-30 minutes. Structure it so it doesn't block developer velocity:
# Strategy: DAST runs on staging after merge to main
# Not on every PR — too slow
on:
push:
branches: [main] # After merge, not on PR
jobs:
dast:
runs-on: ubuntu-latest
environment: staging # Deploy to staging first, then scan
steps:
- name: Deploy to Staging
run: ./deploy.sh staging
- name: Wait for Deployment
run: /usr/local/bin/await 'curl -sf https://api.staging.myapp.com/health'
- name: ZAP Scan
uses: zaproxy/action-api-scan@v0.9.0
with:
target: 'https://api.staging.myapp.com/openapi.json'
fail_action: false # Report but don't block
- name: Notify on New Findings
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: "DAST scan found new vulnerabilities on staging"Summary
| Tool | Best For | Speed | False Positive Rate |
|---|---|---|---|
| OWASP ZAP | Comprehensive crawl + attack | Slow (10-30m) | Medium |
| Nuclei | Template-based CVE + misconfig | Fast (1-5m) | Low |
| Custom scripts | BOLA, business logic | Varies | Minimal |
Run ZAP weekly against staging for comprehensive coverage. Run Nuclei on every deployment for fast CVE and misconfiguration checks. Write custom scripts for your specific business logic vulnerabilities (BOLA is the hardest to automate but the most common API vulnerability).
DAST is not optional for APIs exposed to the internet. It finds real exploits that no amount of code review catches.