OWASP ZAP Security Testing: Automated Web Application Scanning
OWASP ZAP (Zed Attack Proxy) is the most widely used open-source web application security scanner. It acts as a proxy between your browser and the target application, passively scanning traffic for vulnerabilities. It also has an active scanner that sends attack payloads to find SQL injection, XSS, CSRF, and other OWASP Top 10 vulnerabilities. ZAP integrates with CI/CD via Docker — run it as part of your pipeline to catch security regressions before deployment.
What OWASP ZAP Does
ZAP tests web applications by:
- Passive scanning — monitors HTTP traffic between browser and app, flags suspicious patterns without sending attack traffic
- Active scanning — actively sends attack payloads (SQL injection strings, XSS vectors, path traversal attempts) to find vulnerabilities
- Spidering — crawls the application automatically to discover all URLs and endpoints
- AJAX Spider — crawls Single Page Applications that traditional spidering misses
- Fuzzer — sends malformed inputs to specific parameters
ZAP finds vulnerabilities in the OWASP Top 10:
- A01: Broken Access Control
- A02: Cryptographic Failures (missing HTTPS, weak ciphers)
- A03: Injection (SQL, LDAP, command injection)
- A07: XSS (stored, reflected, DOM-based)
- A05: Security Misconfiguration (missing headers, error exposure)
Installation
Desktop GUI (for manual testing): Download from zaproxy.org or:
# macOS
brew install --cask owasp-zap
# Docker (no GUI, for CI)
docker pull ghcr.io/zaproxy/zaproxy:stableCheck version:
docker run --rm ghcr.io/zaproxy/zaproxy:stable zap.sh -versionQuick Start: Baseline Scan with Docker
The fastest way to scan a web application:
docker run --rm \
-v $(pwd):/zap/wrk:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py \
-t https://your-app.example.com \
-r zap-report.html \
-J zap-report.jsonThe baseline scan:
- Passive scan only (no attack traffic)
- Spiders the site
- Reports issues by risk level: High, Medium, Low, Informational
- Safe to run against production
Output:
Total of 4 alerts were raised.
Pass [4]: Directory Browsing [0]
Pass [4]: ...
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 25
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 7
FAIL-NEW: Cookie Without SameSite Attribute [10054] x 3
FAIL-NEW: SQL Injection [40018] x 1Exit codes:
0— No alerts at or above the fail threshold1— Alerts at or above the fail threshold2— Scan failed (can't reach target)
Full Scan (Active Scanning)
The full scan sends attack payloads — run only against non-production:
docker run --rm \
-v $(pwd):/zap/wrk:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py \
-t http://staging.your-app.com \
-r full-scan-report.html \
-J full-scan-report.json \
-m 5 # Maximum spider depthFull scan includes:
- Everything in baseline scan
- Active scanner (injection attacks, XSS vectors)
- Longer spider depth
Full scans take 15–60 minutes depending on application size.
API Scanning
For REST APIs, use the API scan with an OpenAPI/Swagger spec:
docker run --rm \
-v $(pwd):/zap/wrk:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t http://api.your-app.com/v1/openapi.json \
-f openapi \
-r api-scan-report.htmlOr GraphQL:
zap-api-scan.py \
-t http://api.your-app.com/graphql \
-f graphql \
-r api-scan-report.htmlFor SOAP APIs:
zap-api-scan.py \
-t http://api.your-app.com/wsdl \
-f soap \
-r api-scan-report.htmlGitHub Actions Integration
# .github/workflows/security-scan.yml
name: Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
zap-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start application
run: |
docker compose up -d
# Wait for app to be ready
timeout 60 sh -c 'until curl -sf http://localhost:3000/health; do sleep 2; done'
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.11.0
with:
target: 'http://localhost:3000'
rules_file_name: '.zap/rules.tsv'
issue_title: 'ZAP Security Scan Report'
fail_action: false # Don't fail on first scan; review alerts first
- name: Upload ZAP Report
if: always()
uses: actions/upload-artifact@v4
with:
name: zap-report
path: report_html.htmlFor the full scan:
- name: ZAP Full Scan
uses: zaproxy/action-full-scan@v0.10.0
with:
target: 'http://staging.your-app.com'
cmd_options: '-a' # Adjust alerts, don't fail buildConfiguring Alert Thresholds
Customize which alerts pass or fail with a rules file:
# .zap/rules.tsv
# Format: ID Action Parameter (optional)
10021 IGNORE # X-Content-Type-Options - handled by CDN
10038 WARN # CSP header missing
10054 FAIL # Cookie SameSite missing
40018 FAIL # SQL Injection
40012 FAIL # Cross Site ScriptingActions: IGNORE, WARN, FAIL
ZAP GUI for Manual Testing
For manual security testing, the ZAP GUI works as a proxy:
- Launch ZAP GUI
- Configure your browser to use ZAP as a proxy (127.0.0.1:8080)
- Browse your application normally
- ZAP passively scans all traffic and shows findings in real time
Manual testing workflow:
- Spider: Right-click target → Attack → Spider to crawl all links
- Active Scan: Right-click target → Attack → Active Scan
- Fuzzer: Right-click a request → Fuzz to test specific parameters
- Break (intercept): Modify requests before they're sent
Authentication Configuration
Most real-world applications require authentication. Configure ZAP to authenticate:
Form-based authentication:
# Using ZAP CLI with authentication
docker run --rm ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py \
-t http://your-app.com \
-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\ eyJhbGciO..." \
-r authenticated-scan.htmlCookie-based:
-z "-config replacer.full_list(0).matchstr=Cookie \
-config replacer.full_list(0).replacement=session=abc123"ZAP Scripting
Automate complex testing scenarios with Groovy, Python, or JavaScript scripts:
// authentication.groovy
import org.parosproxy.paros.network.HttpMessage
def authenticate(helper, paramsValues, credentials) {
def msg = helper.prepareMessage()
msg.getRequestHeader().setURI(
new org.apache.commons.httpclient.URI("http://app/login", false)
)
def body = "username=${credentials.getParam('username')}" +
"&password=${credentials.getParam('password')}"
msg.setRequestBody(body)
msg.getRequestHeader().setContentLength(msg.getRequestBody().length())
helper.sendAndReceive(msg)
// Return the message containing the auth token/cookie
return msg
}Understanding ZAP Report Findings
ZAP reports findings by risk level:
High Risk — requires immediate attention:
- SQL Injection
- Remote Code Execution
- Authentication bypass
- Stored XSS
Medium Risk — important but not immediately critical:
- CSRF
- Open Redirect
- Security header misconfiguration
Low Risk — best practices violations:
- Missing HTTPS
- Verbose error messages
- Excessive cookie scope
Informational — not vulnerabilities, but worth knowing:
- Technology fingerprinting
- Email addresses in source
- Comments with sensitive information
For each finding:
- Alert name: the vulnerability type
- URL: the affected endpoint
- Parameter: the affected parameter
- Evidence: the response that triggered the alert
- Solution: remediation guidance
ZAP vs Burp Suite
| Aspect | OWASP ZAP | Burp Suite Professional |
|---|---|---|
| Cost | Free | ~$450/user/year |
| Community | Large (OWASP) | Large (PortSwigger) |
| Scanner | Good | Excellent |
| Manual testing | Good | Better (more extensions) |
| CI/CD | Native Docker support | Requires extra setup |
| API scanning | Good | Good |
| False positive rate | Moderate | Lower |
For automated security testing in CI/CD, ZAP is the practical choice — free, Docker-native, and well-maintained. Burp Suite is the preferred tool for manual penetration testing.
Reducing False Positives
ZAP generates some false positives, especially for:
Missing security headers — if your CDN/load balancer adds headers that ZAP doesn't see when scanning directly, add them to the rules file:
10021 IGNORE # X-Content-Type-Options added by CloudFrontAJAX/SPA content — ZAP's spider may not fully explore React/Vue apps. Use the AJAX spider:
zap-full-scan.py -t http://your-app.com -j # Enable AJAX spiderAuthenticated areas — ZAP can't scan authenticated pages without credentials configured. Rather than ignoring these findings, configure authentication properly.
Embedding ZAP in Your Security Pipeline
# Full security testing pipeline
stages:
- name: Unit tests
run: npm test
- name: SAST (static)
run: |
npx eslint src --rule 'no-eval: error'
semgrep --config p/owasp-top-ten src/
- name: Dependency audit
run: npm audit --audit-level=high
- name: Deploy to staging
run: kubectl apply -f k8s/staging/
- name: DAST (dynamic - ZAP)
run: |
docker run --rm ghcr.io/zaproxy/zaproxy:stable \
zap-full-scan.py \
-t http://staging.your-app.com \
-r report.html -J report.json
- name: Deploy to production
when: all_previous_passSAST catches security issues in code before they deploy; DAST (ZAP) catches vulnerabilities in the running application that static analysis can't see.