OWASP ZAP Tutorial: Automated Security Scanning in CI/CD

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 pull zaproxy/zap-stable

ZAP'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.html

This 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.json

The 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.html

ZAP 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: testpassword

Then 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.html

Excluding 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.html

The 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.xml

Handling 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 implementation

Or 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.html

Run it:

docker run --rm \
  -v $(<span class="hljs-built_in">pwd):/zap/wrk:rw \
  zaproxy/zap-stable zap.sh \
  -cmd -autorun /zap/wrk/automation.yaml

What 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.

Read more