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

RiskDescriptionAction Required
HighCritical vulnerabilities (SQL Injection, RCE)Fix immediately
MediumSignificant issues (XSS, weak crypto)Fix before release
LowMinor issues (missing headers)Fix when possible
InformationalBest practice violationsConsider 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.