Post

Week 3 — Day 18: DAST with OWASP ZAP

A full walkthrough of OWASP ZAP for dynamic application security testing — spidering, active scanning, API testing, and running ZAP in CI against a staging environment.

Week 3 — Day 18: DAST with OWASP ZAP

What is DAST?

Dynamic Application Security Testing (DAST) tests a running application by sending real HTTP requests and analyzing the responses — just like a real attacker would.

Unlike SAST (which reads code) and SCA (which reads dependency files), DAST actually interacts with your application at runtime.

What DAST finds that SAST misses:

  • Runtime configuration errors (debug mode on in prod, verbose error messages)
  • Auth bypass vulnerabilities
  • Session management issues (fixation, insecure cookies)
  • Server-side request forgery (SSRF)
  • Misconfigurations in web server headers
  • Race conditions
  • Business logic flaws exposed through the API

What DAST cannot find:

  • Code-level issues not reachable via HTTP
  • Problems in code paths not triggered by the scanner
  • Secrets in source code (use SAST/secret scanning for that)

OWASP ZAP

OWASP ZAP (Zed Attack Proxy) is the most widely used open-source DAST tool. It works as an intercepting proxy + active scanner + automation platform.

Scan modes: | Mode | Description | |——|————-| | Baseline scan | Passive only — no active attacks, safe for production | | Full scan | Active scanning — sends attack payloads, staging only | | API scan | Scans OpenAPI/Swagger/GraphQL endpoints |


Installation

1
2
3
4
5
6
# Download ZAP from https://www.zaproxy.org/download/
# Or via Docker (recommended for CI)
docker pull ghcr.io/zaproxy/zaproxy:stable

# Verify
docker run --rm ghcr.io/zaproxy/zaproxy:stable zap.sh -version

Baseline Scan (Passive — Safe for Any Environment)

The baseline scan crawls the site and runs passive checks only — it reads responses but doesn’t send attack payloads. Safe to run against production.

1
2
3
4
5
6
# Baseline scan against a URL
docker run --rm ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py \
  -t https://your-staging-app.com \
  -r zap-report.html \
  -J zap-report.json

[SCREENSHOT]Terminal showing ZAP baseline scan running — crawling pages, then the summary output showing PASS/WARN/FAIL for each check, and the total alert count by severity

What the baseline scan checks:

  • Missing security headers (X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Strict-Transport-Security)
  • Cookies without HttpOnly or Secure flags
  • Information disclosure (server version in headers, stack traces in errors)
  • Outdated or vulnerable JS libraries
  • CORS misconfiguration

Full Scan (Active — Staging Only)

The full scan adds active attack payloads — it will send SQLi, XSS, and other payloads to your app. Only run this against staging or dedicated test environments.

1
2
3
4
5
6
docker run --rm ghcr.io/zaproxy/zaproxy:stable \
  zap-full-scan.py \
  -t https://staging.yourapp.com \
  -r zap-full-report.html \
  -J zap-full-report.json \
  -m 10       # max crawl time in minutes

[SCREENSHOT]Terminal showing ZAP full scan running — active scanner progress with the number of requests sent, followed by the alert list with severity levels (High/Medium/Low/Informational)


API Scan

If your application exposes an API documented with OpenAPI/Swagger, ZAP can scan it directly:

1
2
3
4
5
6
7
8
# Scan using an OpenAPI spec
docker run --rm \
  -v $(pwd):/zap/wrk:rw \
  ghcr.io/zaproxy/zaproxy:stable \
  zap-api-scan.py \
  -t /zap/wrk/openapi.yaml \
  -f openapi \
  -r api-scan-report.html

[SCREENSHOT]Terminal showing ZAP API scan parsing the OpenAPI spec, listing the endpoints it found, and then scanning each one — followed by the alert summary


Understanding ZAP Alerts

ZAP categorizes alerts by risk level:

RiskDescription
HighCritical vulnerabilities — SQLi, XSS, SSRF, command injection
MediumSignificant weaknesses — missing security headers, CSRF, path traversal
LowMinor issues — info disclosure, verbose error messages
InformationalNot vulnerabilities — just observations

[SCREENSHOT]ZAP HTML report opened in browser showing the alerts tree on the left grouped by risk level, and a specific High alert expanded on the right showing the URL, parameter, evidence, and solution

Each alert includes:

  • URL — which endpoint triggered it
  • Parameter — which parameter was vulnerable
  • Attack — the payload ZAP used
  • Evidence — what in the response confirmed the vulnerability
  • Solution — how to fix it
  • CWE — the relevant weakness category

ZAP with DVWA (Intentionally Vulnerable App)

Practice against DVWA (Damn Vulnerable Web Application):

1
2
3
4
5
6
7
8
9
10
# Start DVWA
docker run -d -p 80:80 vulnerables/web-dvwa

# Run ZAP baseline against it
docker run --rm \
  --network host \
  ghcr.io/zaproxy/zaproxy:stable \
  zap-baseline.py \
  -t http://localhost/dvwa \
  -r dvwa-report.html

[SCREENSHOT]DVWA running in browser showing the login page and dashboard, confirming the vulnerable app is accessible

[SCREENSHOT]ZAP report for DVWA showing multiple High alerts — SQL Injection, Reflected XSS, command injection — with the specific parameters and evidence


Security Headers — Common Findings

One of the most common ZAP findings is missing security headers. Here’s what each header does and how to set it:

Content-Security-Policy (CSP)

Prevents XSS by restricting script sources:

1
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'

Strict-Transport-Security (HSTS)

Forces HTTPS:

1
Strict-Transport-Security: max-age=31536000; includeSubDomains

X-Content-Type-Options

Prevents MIME sniffing:

1
X-Content-Type-Options: nosniff

X-Frame-Options

Prevents clickjacking:

1
X-Frame-Options: DENY

Referrer-Policy

Controls referrer information leakage:

1
Referrer-Policy: strict-origin-when-cross-origin

In Nginx:

1
2
3
4
5
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

[SCREENSHOT]Browser dev tools → Network → response headers for the app after adding security headers — showing all the above headers present with their values


ZAP Automation Framework

For complex CI scenarios, ZAP has an automation framework driven by a YAML config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# zap-automation.yaml
env:
  contexts:
    - name: app-context
      urls:
        - https://staging.yourapp.com
      authentication:
        method: form
        parameters:
          loginUrl: https://staging.yourapp.com/login
          usernameField: username
          passwordField: password
          username: testuser
          password: testpass

jobs:
  - type: spider
    parameters:
      maxDuration: 5

  - type: activeScan
    parameters:
      policy: Default Policy

  - type: report
    parameters:
      template: traditional-html
      reportDir: /zap/wrk/reports/
      reportFile: zap-report.html
1
2
3
4
docker run --rm \
  -v $(pwd):/zap/wrk:rw \
  ghcr.io/zaproxy/zaproxy:stable \
  zap.sh -cmd -autorun /zap/wrk/zap-automation.yaml

Integrating ZAP into GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# .github/workflows/dast.yml
name: DAST — ZAP Scan

on:
  push:
    branches: [main]

jobs:
  zap-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Start staging app
        run: docker compose up -d --wait

      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: http://localhost:3000
          rules_file_name: .zap/rules.tsv
          cmd_options: -J zap-report.json

      - name: Upload ZAP report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap-report
          path: zap-report.html

[SCREENSHOT]GitHub Actions run showing the ZAP baseline scan step — output shows alerts found with their risk level, and the step either passes or fails based on the threshold configured

ZAP rules file — control which alerts fail the build:

# .zap/rules.tsv
# Rule ID  Action  Description
10020      FAIL    X-Frame-Options header missing
10021      FAIL    X-Content-Type-Options missing
10038      WARN    Content Security Policy missing
10098      IGNORE  Cross-Domain Misconfiguration (low risk env)
  • FAIL — the step fails (non-zero exit code)
  • WARN — logged but doesn’t fail the build
  • IGNORE — not reported

Lab — Baseline Scan Against DVWA or Juice Shop

Objective: Run a ZAP baseline scan against a local vulnerable app and review the report.

  1. Start Juice Shop locally:
    1
    
    docker run -d -p 3000:3000 bkimminich/juice-shop
    
  2. Run ZAP baseline:
    1
    2
    3
    4
    5
    6
    7
    8
    
    docker run --rm \
      --network host \
      -v $(pwd)/reports:/zap/wrk:rw \
      ghcr.io/zaproxy/zaproxy:stable \
      zap-baseline.py \
      -t http://localhost:3000 \
      -r /zap/wrk/juice-shop-baseline.html \
      -J /zap/wrk/juice-shop-baseline.json
    

[SCREENSHOT]Terminal showing ZAP scanning Juice Shop — the crawl progress, then the alert summary table with risk counts

  1. Open reports/juice-shop-baseline.html in a browser

[SCREENSHOT]ZAP HTML report for Juice Shop opened in browser — showing High/Medium/Low/Info alert counts, and the list of findings with their URLs

  1. Pick 2 Medium findings → look at what header or config is missing → implement the fix in Nginx or the app config

  2. Re-run the scan → confirm the fixed alert is gone from the report

[SCREENSHOT]Second ZAP scan report showing fewer alerts after the security headers were added — the previously failing header checks now absent


Key Takeaways

  • DAST tests the running app — it finds what SAST and SCA miss because it interacts with real responses
  • Baseline scan is safe for any environment — no attack payloads, just passive observation
  • Full/active scan only against staging — never production
  • Security headers are the most common DAST finding and the easiest to fix — add them in Nginx/ALB config
  • ZAP in CI as a baseline scan gives you continuous coverage on every deployment
  • Use a rules file (.tsv) to decide what fails the build vs what’s just a warning
  • Combine SAST + SCA + IaC scan + DAST in sequence for a full pipeline security gate

References


You can find me online at:

My signature image

This post is licensed under CC BY 4.0 by the author.