What is OWASP ZAP?
OWASP (as discussed in Penetration Testing Basics for QA Testers) Zed Attack Proxy (ZAP) is the world’s most popular free security testing tool, actively maintained by the OWASP community. For QA engineers, ZAP provides automated security scanning to detect vulnerabilities like SQL (as discussed in Security Testing for QA: A Practical Guide) injection, XSS, and insecure configurations before they reach production.
Why ZAP for QA?
- Free and Open Source - No licensing costs, full feature access
- Easy to Automate - CLI, Docker, APIs for CI/CD integration
- Comprehensive Scanning - Active/passive scanning, spidering, fuzzing
- API Testing - OpenAPI/Swagger support, GraphQL testing
- Extensive Reporting - HTML, JSON, XML reports with remediation guidance
- Active Community - Regular updates, plugins, marketplace
ZAP Scanning Modes
1. Passive Scanning
Analyzes HTTP traffic without sending additional requests. Safe to run in production.
# Start ZAP in daemon mode
docker run -u zap -p 8080:8080 owasp/zap2docker-stable zap.sh -daemon \
-host 0.0.0.0 -port 8080 -config api.disablekey=true
# Access target through ZAP proxy
curl -x http://localhost:8080 https://example.com
# Generate passive scan report
curl "http://localhost:8080/JSON/core/action/jsonreport/"
Passive Scan Detects:
- Missing security headers (CSP, X-Frame-Options, HSTS)
- Insecure cookies (missing HttpOnly, Secure flags)
- Information disclosure (stack traces, comments in code)
- Outdated libraries and frameworks
2. Active Scanning
Sends attack payloads to find vulnerabilities. Only use on authorized targets.
# Spider target to discover URLs
docker run owasp/zap2docker-stable zap-baseline.py \
-t https://example.com \
-r report.html
# Full active scan
docker run owasp/zap2docker-stable (as discussed in [SQL Injection and XSS: Finding Vulnerabilities](/blog/sql-injection-xss)) zap-full-scan.py \
-t https://example.com \
-r full-report.html \
-z "-config api.disablekey=true"
Active Scan Detects:
- SQL Injection
- Cross-Site Scripting (XSS)
- Path Traversal
- Remote Code Execution
- XML External Entity (XXE)
- Server-Side Request Forgery (SSRF)
3. API Scanning
Test REST APIs using OpenAPI/Swagger specifications.
# Import OpenAPI spec and scan
docker run -v $(pwd):/zap/wrk:rw owasp/zap2docker-stable \
zap-api-scan.py \
-t https://api.example.com \
-f openapi \
-d /zap/wrk/openapi.json \
-r /zap/wrk/api-report.html
# Python API automation
from zapv2 import ZAPv2
import time
# Connect to ZAP
zap = ZAPv2(proxies={'http': 'http://localhost:8080', 'https': 'http://localhost:8080'})
# Import OpenAPI definition
zap.openapi.import_url('https://api.example.com/openapi.json')
# Spider the API
zap.spider.scan('https://api.example.com')
while int(zap.spider.status()) < 100:
print(f'Spider progress: {zap.spider.status()}%')
time.sleep(2)
# Active scan
zap.ascan.scan('https://api.example.com')
while int(zap.ascan.status()) < 100:
print(f'Active scan progress: {zap.ascan.status()}%')
time.sleep(5)
# Get alerts
alerts = zap.core.alerts(baseurl='https://api.example.com')
print(f'Found {len(alerts)} vulnerabilities')
# Generate report
zap.core.htmlreport()
CI/CD Integration
GitHub Actions
# .github/workflows/security-scan.yml
name: OWASP ZAP Security Scan
on:
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
zap_scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build application
run: docker-compose up -d
- name: Wait for app to start
run: sleep 30
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'http://localhost:3000'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
- name: Upload ZAP Report
uses: actions/upload-artifact@v3
if: always()
with:
name: zap-report
path: report_html.html
Jenkins Pipeline
// Jenkinsfile
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker-compose up -d'
sh 'sleep 30'
}
}
stage('OWASP ZAP Scan') {
steps {
script {
docker.image('owasp/zap2docker-stable').inside('--network=host') {
sh '''
zap-baseline.py \
-t http://localhost:3000 \
-r zap-report.html \
-J zap-report.json \
-w zap-report.md
'''
}
}
}
}
stage('Process Results') {
steps {
publishHTML([
reportDir: '.',
reportFiles: 'zap-report.html',
reportName: 'ZAP Security Report'
])
script {
def report = readJSON file: 'zap-report.json'
def highAlerts = report.site.alerts.findAll { it.riskcode == '3' }
if (highAlerts.size() > 0) {
error("Found ${highAlerts.size()} high-severity vulnerabilities")
}
}
}
}
}
post {
always {
sh 'docker-compose down'
}
}
}
Custom Scan Policies
rules.tsv Configuration
# Ignore false positives
10021 IGNORE (CSP: Wildcard Directive)
10038 IGNORE (Content Security Policy Header Not Set)
# Fail on specific vulnerabilities
40012 FAIL (Cross Site Scripting - Reflected)
40014 FAIL (Cross Site Scripting - Persistent)
90018 FAIL (SQL Injection)
40018 FAIL (SQL Injection - MySQL)
# Warn on medium severity
10054 WARN (Cookie Without SameSite Attribute)
10063 WARN (Permissions Policy Header Not Set)
Python Custom Policy
# custom_zap_scan.py
from zapv2 import ZAPv2
import json
zap = ZAPv2(proxies={'http': 'http://localhost:8080'})
# Configure scan policy
policy_name = 'CustomPolicy'
zap.ascan.new_scan_policy(policy_name)
# Enable specific scanners
scanners = {
'40012': 'MEDIUM', # XSS Reflected
'40014': 'MEDIUM', # XSS Persistent
'40018': 'HIGH', # SQL Injection
'90019': 'HIGH', # Server Side Code Injection
}
for scanner_id, strength in scanners.items():
zap.ascan.set_scanner_alert_threshold(
id=scanner_id,
alertthreshold=strength,
scanpolicyname=policy_name
)
# Start scan with custom policy
scan_id = zap.ascan.scan(
url='https://example.com',
scanpolicyname=policy_name
)
print(f'Scan started with ID: {scan_id}')
Advanced Authentication
Form-Based Authentication
from zapv2 import ZAPv2
zap = ZAPv2()
# Configure context
context_name = 'MyApp'
context_id = zap.context.new_context(context_name)
# Set authentication method
auth_method_config = '''
{
"loginUrl": "https://example.com/login",
"loginRequestData": "username={%username%}&password={%password%}",
"usernameParameter": "username",
"passwordParameter": "password"
}
'''
zap.authentication.set_authentication_method(
contextid=context_id,
authmethodname='formBasedAuthentication',
authmethodconfigparams=auth_method_config
)
# Add user credentials
user_id = zap.users.new_user(context_id, 'testuser')
zap.users.set_authentication_credentials(
contextid=context_id,
userid=user_id,
authcredentialsconfigparams='username=admin&password=password123'
)
zap.users.set_user_enabled(context_id, user_id, True)
# Force user authentication before scanning
zap.forcedUser.set_forced_user(context_id, user_id)
zap.forcedUser.set_forced_user_mode_enabled(True)
API Key Authentication
# API key in header
zap.replacer.add_rule(
description='API Key Header',
enabled=True,
matchtype='REQ_HEADER',
matchstring='Authorization',
replacement='Bearer YOUR_API_KEY',
initiators=''
)
# API key in query parameter
target_url = 'https://api.example.com?apikey=YOUR_API_KEY'
zap.spider.scan(target_url)
Interpreting Results
Risk Levels
Risk | Description | Action Required |
---|---|---|
High | Critical vulnerabilities (SQL Injection, RCE) | Fix immediately |
Medium | Significant issues (XSS, weak crypto) | Fix before release |
Low | Minor issues (missing headers) | Fix when possible |
Informational | Best practice violations | Consider fixing |
Common Vulnerabilities & Fixes
SQL Injection:
# Vulnerable
query = f"SELECT * FROM users WHERE id = {user_input}"
# Fixed - Use parameterized queries
cursor.execute("SELECT * FROM users WHERE id = ?", (user_input,))
XSS:
// Vulnerable
document.getElementById('output').innerHTML = userInput;
// Fixed - Escape output
document.getElementById('output').textContent = userInput;
Missing Security Headers:
# Add to nginx.conf
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header Content-Security-Policy "default-src 'self'";
add_header Strict-Transport-Security "max-age=31536000";
Reporting and Tracking
Generate Comprehensive Reports
# HTML report
zap-cli --zap-url http://localhost:8080 report -o zap-report.html -f html
# JSON report for automation
zap-cli report -o zap-report.json -f json
# Markdown for documentation
zap-cli report -o zap-report.md -f md
Track Vulnerabilities Over Time
# store_scan_results.py
import json
from datetime import datetime
import sqlite3
def store_scan_results(scan_date, alerts):
conn = sqlite3.connect('security_scans.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS scans (
id INTEGER PRIMARY KEY,
scan_date TEXT,
alert_name TEXT,
risk TEXT,
url TEXT,
description TEXT
)
''')
for alert in alerts:
cursor.execute('''
INSERT INTO scans (scan_date, alert_name, risk, url, description)
VALUES (?, ?, ?, ?, ?)
''', (scan_date, alert['alert'], alert['risk'], alert['url'], alert['description']))
conn.commit()
conn.close()
# Load ZAP results
with open('zap-report.json') as f:
data = json.load(f)
store_scan_results(
datetime.now().isoformat(),
data['site']['alerts']
)
Conclusion
OWASP ZAP is essential for integrating security testing into QA workflows. From automated CI/CD scans to comprehensive API testing, ZAP helps identify vulnerabilities early when they’re cheapest to fix.
Key Takeaways:
- Start with baseline scans in CI/CD
- Use passive scanning for production monitoring
- Implement custom policies for your risk tolerance
- Automate authentication for authenticated scanning
- Track vulnerabilities over time
- Fix high/critical issues immediately
Security testing isn’t optional — make ZAP a standard part of your testing process.