OWASP ZAP Tutorial: Automated Security Scanning in CI/CD
OWASP ZAP (Zed Attack Proxy) is the world's most widely used open-source web application security scanner. It finds vulnerabilities automatically — SQL injection, XSS, broken authentication, insecure headers — and integrates into CI/CD pipelines so security issues get caught before they reach production.
This tutorial covers setting up ZAP, running your first scan, and automating it in GitHub Actions.
What OWASP ZAP Does
ZAP acts as a proxy between your browser and the application. It intercepts, analyzes, and actively probes requests to find security weaknesses.
Two primary modes:
Passive scanning — ZAP observes traffic and flags issues without sending additional requests. Zero risk of breaking anything. Good for finding missing security headers, information disclosure, and cookie misconfigurations.
Active scanning — ZAP actively attacks the application by sending malformed inputs, injection payloads, and boundary values. Finds vulnerabilities that passive scanning misses. Never run this against production.
Installation
Desktop (GUI)
Download the installer from zaproxy.org. Available for Windows, macOS, and Linux. The GUI is useful for learning and manual testing.
Docker (Recommended for CI)
docker pull zaproxy/zap-stableZAP's official Docker images include everything needed for automated scanning. No installation on the host machine required.
Running Your First Scan
Passive Scan with Docker
The quickest way to scan a URL:
docker run --rm zaproxy/zap-stable zap-baseline.py \
-t https://staging.example.com \
-r zap-report.htmlThis runs the baseline scan — passive scanning plus a few active checks for high-confidence findings. Takes 2-5 minutes for most applications.
The output shows findings by risk level: High, Medium, Low, Informational.
Full Active Scan
docker run --rm zaproxy/zap-stable zap-full-scan.py \
-t https://staging.example.com \
-r zap-full-report.html \
-J zap-full-report.jsonThe full scan adds active attack simulation. Expect 10-60 minutes depending on application size.
API Scan
If you have an OpenAPI/Swagger spec:
docker run --rm zaproxy/zap-stable zap-api-scan.py \
-t https://api.staging.example.com/openapi.json \
-f openapi \
-r zap-api-report.htmlZAP imports the spec and tests every endpoint with attack payloads.
Understanding ZAP Reports
ZAP categorizes findings by risk:
| Risk Level | Examples | Action |
|---|---|---|
| High | SQL injection, command injection, path traversal | Block deployment, fix immediately |
| Medium | Missing CSRF tokens, weak SSL, clickjacking | Fix before release |
| Low | Cookie flags, verbose error messages | Fix in regular maintenance |
| Informational | Information disclosure, deprecated features | Review and track |
Each finding includes:
- Description — what the vulnerability is
- Evidence — the request/response that triggered it
- Solution — how to fix it
- CWE/OWASP reference — classification
Configuring ZAP for Your Application
Authentication
For authenticated scans, ZAP needs to log in. Create a zap-auth.yaml configuration:
env:
contexts:
- name: MyApp
urls:
- https://staging.example.com
authentication:
method: form
parameters:
loginUrl: https://staging.example.com/login
loginRequestData: username=testuser&password=testpassword
sessionManagement:
method: cookie
users:
- name: testuser
credentials:
username: testuser
password: testpasswordThen run the authenticated scan:
docker run --rm \
-v $(<span class="hljs-built_in">pwd):/zap/wrk:rw \
zaproxy/zap-stable zap-baseline.py \
-t https://staging.example.com \
-c zap-auth.yaml \
-r zap-report.htmlExcluding Paths
Some paths (logout, destructive actions) should be excluded from active scanning:
jobs:
- type: passiveScan-wait
- type: activeScan
parameters:
excludePaths:
- .*/logout.*
- .*/delete.*
- .*/reset.*CI/CD Integration
GitHub Actions
Add ZAP to your pipeline to catch vulnerabilities on every pull request:
name: Security Scan
on:
pull_request:
branches: [main]
jobs:
zap-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Start application
run: docker-compose up -d
# Wait for app to be ready
- name: Run ZAP baseline scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'http://localhost:3000'
fail_action: true
allow_issue_writing: true
rules_file_name: '.zap/rules.tsv'
- name: Upload ZAP report
uses: actions/upload-artifact@v4
if: always()
with:
name: zap-report
path: report_html.htmlThe zaproxy/action-baseline action handles Docker pull, scan execution, and report generation automatically.
Controlling What Fails the Build
Create .zap/rules.tsv to configure which findings block deployment:
10202 IGNORE (Absence of Anti-CSRF Tokens)
10038 WARN (Content Security Policy (CSP) Header Not Set)
40012 FAIL (Cross Site Scripting (Reflected))
40014 FAIL (Cross Site Scripting (Persistent))
90019 FAIL (Server Side Code Injection)FAIL = breaks the build. WARN = reported but doesn't block. IGNORE = skipped entirely.
GitLab CI
zap-security-scan:
image: zaproxy/zap-stable
stage: test
script:
- zap-baseline.py
-t $STAGING_URL
-r zap-report.html
-J zap-report.json
-x zap-report.xml
artifacts:
when: always
paths:
- zap-report.html
- zap-report.json
reports:
junit: zap-report.xmlHandling False Positives
ZAP occasionally flags issues that are not real vulnerabilities in your context. Mark them in the rules file:
10202 IGNORE Known false positive — our custom CSRF implementationOr suppress specific alerts in the ZAP GUI and export the context file for use in CI.
ZAP Automation Framework
For complex scanning workflows, ZAP's Automation Framework replaces CLI flags with a declarative YAML configuration:
env:
contexts:
- name: staging
urls:
- https://staging.example.com
jobs:
- type: spider
parameters:
context: staging
maxDuration: 5
- type: passiveScan-wait
- type: activeScan
parameters:
context: staging
policy: Default Policy
- type: report
parameters:
template: traditional-html
reportDir: /zap/wrk/
reportFile: zap-report.htmlRun it:
docker run --rm \
-v $(<span class="hljs-built_in">pwd):/zap/wrk:rw \
zaproxy/zap-stable zap.sh \
-cmd -autorun /zap/wrk/automation.yamlWhat ZAP Finds (and What It Doesn't)
ZAP finds:
- SQL injection and XSS via active scanning
- Missing security headers (HSTS, CSP, X-Frame-Options)
- Insecure cookie configuration
- Information disclosure in responses
- Open redirects
- Path traversal
ZAP does not find:
- Business logic vulnerabilities
- Authorization flaws (IDOR)
- Race conditions
- Vulnerabilities requiring deep knowledge of the application
For the things ZAP misses, use functional security testing — testing your application's access control rules directly by simulating what real users and attackers would do.
When to Run Which Scan
| Scan Type | When | Time | Risk to App |
|---|---|---|---|
| Baseline | Every PR | 2-5 min | None |
| API scan | API changes | 5-15 min | Low |
| Full active | Weekly, staging only | 10-60 min | Medium |
| Authenticated | Before release | 15-30 min | Medium |
Never run active scans against production. The attack payloads can corrupt data and affect real users.
Common Issues
ZAP can't reach the app: Use host.docker.internal on Docker Desktop instead of localhost, or use --network host on Linux.
Authentication fails: Check that your login credentials and form field names match exactly. Use the ZAP GUI to record a login session and export it.
Too many false positives: Start with the baseline scan and tune your rules file before moving to active scanning.
Scan takes too long: Limit scope with excludePaths, reduce the spider depth, or use the API scan with an OpenAPI spec instead of crawling.
Combining ZAP with Functional Security Tests
ZAP handles automated vulnerability scanning. It doesn't handle testing your application-specific security rules — like whether a regular user can access the admin panel, or whether Customer A can view Customer B's orders.
Those tests require understanding your application's business logic. Write them as part of your regular test suite, running against the same staging environment where ZAP scans.
The combination covers both attack-pattern vulnerabilities (ZAP's domain) and functional security failures (your test suite's domain) — the two layers that matter most.