OWASP ZAP in CI/CD: Automated Security Testing for Web Applications
A practical guide to running OWASP ZAP in Docker for CI/CD pipelines—passive vs active scanning, authenticated scanning for protected endpoints, generating SARIF output for GitHub Code Scanning, and managing scan results without drowning in false positives.
What OWASP ZAP Provides
OWASP ZAP (Zed Attack Proxy) is the most widely used open source Dynamic Application Security Testing (DAST) tool. Unlike SAST tools that analyze source code, ZAP actually runs against a deployed application, making HTTP requests and observing responses. This means ZAP can find vulnerabilities that are only visible at runtime: SQL injection that emerges from complex query construction, XSS that depends on the server's response transformation, authentication bypasses, and misconfigured HTTP headers.
The key distinction between DAST and SAST: SAST has zero false negatives for patterns it recognizes (if the vulnerable code exists, it will find it) but high false positives (the code may be safe in its execution context). DAST confirms actual exploitability—if ZAP triggers an XSS alert, it actually injected a payload and received it back—but may miss vulnerabilities that require specific user flows or data states.
ZAP in CI/CD provides the DAST layer in a DevSecOps pipeline, catching a class of vulnerabilities that code-level analysis misses entirely.
ZAP Docker Modes
ZAP provides official Docker images that are designed for CI/CD use. There are three primary scan modes:
Baseline Scan: Passive scanning only. ZAP spiders the application to discover URLs, then analyzes all traffic for issues without sending attack payloads. Fast (2–5 minutes), zero risk of disrupting the application, but catches only passive findings: missing security headers, information disclosure in responses, insecure configurations.
Full Scan: Active scanning. After spidering, ZAP sends attack payloads to each parameter, form, and URL to probe for vulnerabilities. More thorough but slower (10–60 minutes depending on application size) and can cause side effects (creating test records, triggering rate limiting, potentially disrupting a shared environment).
API Scan: Specifically designed for REST and GraphQL APIs. Takes an API specification (OpenAPI/Swagger, GraphQL introspection) as input and tests all defined endpoints.
Basic GitHub Actions Integration
The ZAP team maintains official GitHub Actions that make integration straightforward:
name: ZAP Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
zap-baseline:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Deploy the application to a test environment
# (varies by your deployment setup)
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.yourapp.com'
# Fail the action if ZAP finds new alerts
fail_action: true
# GitHub issue title for alerts (optional)
issue_title: 'ZAP Baseline Scan Results'
# Optional: custom rules file
rules_file_name: '.zap/rules.tsv'
# Optional: custom configuration
cmd_options: '-a -j'
zap-full-scan:
# Only run full scan on main branch (not every PR)
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: ZAP Full Scan
uses: zaproxy/action-full-scan@v0.10.0
with:
target: 'https://staging.yourapp.com'
fail_action: false # Review results, don't block main branch
issue_title: 'ZAP Full Scan Results'
Passive vs Active Scanning: What Each Finds
Passive Scan Findings (Baseline)
Passive scanning analyzes responses without sending attack payloads. Common findings:
- Missing security headers:
X-Frame-Options,X-Content-Type-Options,Strict-Transport-Security,Content-Security-Policy - Information disclosure: Server header revealing version, X-Powered-By header, debug information in responses, comments in HTML source
- Cookie issues: Session cookies without
HttpOnly,Secure, orSameSiteflags - Insecure content: HTTP resources loaded on HTTPS pages
- Timestamps in responses: Last-Modified headers revealing system timezone
These findings are safe to check on production or staging environments with no risk of disruption.
Active Scan Findings (Full Scan)
Active scanning sends attack payloads and looks for vulnerable responses. Common findings:
- SQL injection: ZAP sends
' OR '1'='1and variants, looks for SQL error messages or behavioral changes - Cross-site scripting (XSS): Injects
<script>alert(1)</script>and variants, checks if payload is reflected unsanitized - Command injection: Sends OS command separators, looks for timing differences or error output
- Path traversal: Attempts
../../../etc/passwdin file parameters - SSRF: Attempts to make the server issue requests to internal addresses
- XML external entity (XXE): If XML input is accepted, attempts entity injection
- CRLF injection: Sends
%0d%0ain headers/cookies, looks for injected headers in responses
Important: Only run active scans against environments you own and have authorization to test. Active scanning can create records in your database, trigger outgoing requests from your server, and cause rate limiting.
Authenticated Scanning
The most significant limitation of unauthenticated ZAP scans is that they cannot test endpoints behind authentication. For a typical web application, 80%+ of the attack surface requires an authenticated session.
ZAP supports several authentication mechanisms:
Form-Based Authentication Script
ZAP uses automation scripts to handle login. The most reliable approach for CI/CD is using ZAP's automation framework with a saved session:
# automation/auth-plan.yaml - ZAP Automation Framework configuration
env:
contexts:
- name: "MyApp"
urls:
- "https://staging.yourapp.com"
includePaths:
- "https://staging.yourapp.com/.*"
excludePaths:
- "https://staging.yourapp.com/logout.*"
authentication:
method: form
parameters:
loginPageUrl: "https://staging.yourapp.com/login"
loginRequestData: "email={%username%}&password={%password%}"
loginPageRequest: "POST"
verification:
method: response
loggedInRegex: "\"userId\":"
loggedOutRegex: "\"error\":\"Authentication required\""
jobs:
- type: spider
parameters:
context: "MyApp"
user: "test-user"
- type: activeScan
parameters:
context: "MyApp"
user: "test-user"
policy: "API-Scan-Policy"
# GitHub Actions step using automation framework
- name: ZAP Authenticated Scan
run: |
docker run --network host \
-v $(pwd)/automation:/zap/wrk/:rw \
-e ZAP_AUTH_HEADER_VALUE="${{ secrets.ZAP_AUTH_TOKEN }}" \
ghcr.io/zaproxy/zaproxy:stable zap.sh \
-cmd -autorun /zap/wrk/auth-plan.yaml
Bearer Token Authentication
For API testing where you can pre-generate an auth token:
- name: ZAP API Scan with Bearer Token
uses: zaproxy/action-api-scan@v0.7.0
with:
target: 'https://staging.yourapp.com/api/openapi.json'
format: openapi
fail_action: true
cmd_options: >
-config replacer.full_list\(0\).matchtype=REQ_HEADER
-config replacer.full_list\(0\).matchstr=Authorization
-config replacer.full_list\(0\).replacement="Bearer ${{ secrets.STAGING_API_TOKEN }}"
Creating a Test User for Scanning
For robust authenticated scanning, create a dedicated test user in your staging environment with:
- Known credentials (stored as CI secrets)
- Representative permissions (not admin, but typical user role)
- Data that simulates a real user without impacting real user data
- Cleanup job to reset the test user's data after each scan run
This is safer than using a real admin account, which could cause damage if ZAP's active scan triggers unexpected mutations.
SARIF Output for GitHub Code Scanning
SARIF (Static Analysis Results Interchange Format) is the standard format for security tool output that integrates with GitHub's Code Scanning feature. Uploading ZAP results as SARIF displays them as annotations on the code and in the Security tab.
- name: ZAP Baseline Scan with SARIF
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.yourapp.com'
fail_action: false # Don't fail; we'll use SARIF upload instead
artifact_name: 'zap-results'
- name: Upload SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results/zap.sarif
category: dast
ZAP generates SARIF by default when using the GitHub Actions. The SARIF file will be in the results/ directory after the scan.
Managing Scan Results and False Positives
ZAP baseline scans often produce false positives—alerts that are not real vulnerabilities in your specific context. Managing these is essential to avoid alert fatigue.
Rules Configuration File
Create a .zap/rules.tsv file to configure how specific alert IDs are treated:
# Rule ID Action Parameter Evidence
# IGNORE = don't report
# WARN = report as warning (default)
# FAIL = fail the scan on this alert
# Content Security Policy header missing - we handle CSP via CDN
10038 IGNORE
# X-Frame-Options header not set - we use CSP frame-ancestors instead
10020 IGNORE
# Missing Anti-CSRF Tokens - our API is stateless with JWT
10202 IGNORE
# Fail on these
90019 FAIL # Server Side Code Injection
40018 FAIL # SQL Injection
90011 FAIL # Cross Site Scripting (Reflected)
Active Scan Policies
For full scans, create a custom scan policy to:
- Exclude tests that are not relevant to your application type
- Tune strength and threshold per test category
- Skip tests that require human interaction
<!-- policies/api-policy.xml -->
<policy>
<name>API-Scan-Policy</name>
<description>Focused policy for REST API scanning</description>
<scanner id="40018" enabled="true" strength="HIGH" threshold="MEDIUM"/> <!-- SQL Injection -->
<scanner id="90011" enabled="true" strength="HIGH" threshold="MEDIUM"/> <!-- XSS -->
<scanner id="90019" enabled="true" strength="MEDIUM" threshold="MEDIUM"/><!-- Server-side Injection -->
<scanner id="10202" enabled="false"/> <!-- CSRF - not applicable to APIs -->
<scanner id="90020" enabled="false"/> <!-- XPath Injection - not applicable -->
</policy>
Baseline in CI, Full Scan Nightly
A practical scan strategy:
- Every PR: Baseline scan (passive, fast, low noise). Block merge on new critical findings.
- Every merge to main: Baseline scan + API scan if OpenAPI spec exists.
- Nightly on staging: Full active scan. Review findings in the morning; track trend over time.
- Pre-release: Full active scan with authenticated testing. Required to pass before production deployment.
This balances thorough coverage against CI pipeline speed. The nightly full scan provides comprehensive coverage without slowing down PR feedback loops.
Integrating with Security Tooling
ZAP findings integrate well with broader security programs:
Defect tracking: Configure ZAP to post findings to GitHub Issues, Jira, or your security tracking system. For CI scans, group findings by URL and severity rather than creating one issue per finding.
Trend tracking: Track findings over time. A baseline that starts at 15 medium findings and trends toward 0 demonstrates security improvement. A trend toward 50 findings indicates security debt accumulation.
Penetration test preparation: ZAP baseline results provide a starting point for human penetration testers. Triage and resolve all ZAP findings before scheduling a pentest—this lets the pentest focus on complex business logic vulnerabilities that automated tools cannot find, rather than spending time on issues a scanner could catch.
ZAP is not a replacement for human security review, but it is an accessible, free tool that catches entire categories of common vulnerabilities automatically, providing security coverage at a cost and speed that manual testing cannot match for every build.