OWASP ZAP
OWASP ZAP — the Zed Attack Proxy — is the free, open-source dynamic application security testing (DAST) tool most pipelines reach for first, and the DAST scanner the Certified DevSecOps Practitioner curriculum names directly. It's an intercepting proxy: it sits between a client and a running target, watches every request and response that passes through it, and — if you ask it to — attacks that target the way an external adversary would, with no access to source code at all. It ships as a desktop application for manual testing, a Docker image built for headless CI, and a REST API that both of those front ends are really just clients of. This page goes one tool deep on what's actually different between a baseline and a full active scan, how to drive either one from a committed zap.yaml instead of hand-clicking through a GUI wizard, and the authentication and session-handling setup that decides whether a scan covers the real application or quietly stalls at the login form.
Imagine testing how secure a house is, but you're not allowed to see the blueprints — you can only be a visitor at the front door. OWASP ZAP is like a very thorough, very patient visitor. First it walks the whole perimeter, quietly noting which windows and doors look unlocked without ever touching a handle — that's the safe, "baseline" pass. Then, only if you ask it to, it comes back and actually tries every one it noted: rattling handles, pushing on windows, seeing if a lock is really as sturdy as it looks — that's the active scan. It never once sees the floor plan; it only ever learns what a stranger walking the outside could learn.
What ZAP is, and the problem it solves
☺ Like you're 10: It's a proxy that watches — and, only if you ask, attacks — real network traffic to a real running app. Nothing about it ever reads a line of your source code.
ZAP began as a fork of the discontinued Paros Proxy, built by Simon Bennetts and donated to OWASP in 2010, where it spent well over a decade as an OWASP Flagship project. Project stewardship and branding have shifted since — governance moved toward a dedicated foundation, with Checkmarx as a major sponsor and employer of core maintainers — so if the exact organizational home matters for a procurement or compliance question, confirm it on the project's own site rather than trust any single name here; the CDP curriculum and most of the industry still just call it "OWASP ZAP" regardless. What hasn't moved is the license (Apache 2.0, genuinely free) or the core design: a single tool that's simultaneously a desktop GUI for manual exploratory testing, a Docker image with packaged scan scripts for CI, and a REST/JSON API that both of those front ends are ultimately built on top of.
The gap ZAP fills is the one SAST, DAST & SCA already named: static analysis reads code that was never actually run, so it's structurally blind to anything that only exists once a request hits a live server — a missing Content-Security-Policy header, a session cookie without the Secure flag, an authorization check that two middleware layers quietly skip when they interact a particular way at runtime. ZAP catches exactly that category, because it isn't reading anything — it's watching, and attacking, the actual HTTP traffic a running instance produces.
DAST's whole value proposition is that it needs no source access — which is also its whole limitation. A ZAP finding is a symptom: a request, a response, a payload that produced something suspicious. It is not a source location. Someone still has to trace that HTTP evidence back to the line of code that produced it, which is exactly why DAST findings and SAST findings end up triaged side by side rather than one replacing the other.
Architecture: the proxy, two spiders, and two very different scanners
☺ Like you're 10: Four moving parts do almost all the work: a proxy that watches, two different ways of finding pages, a scanner that only looks, and a scanner that actually attacks.
Everything else in ZAP hangs off one core fact: it's a man-in-the-middle HTTP(S) proxy. Point a browser or an API client at it, and every request and response that flows through gets recorded into the History tab and organized into the Sites tree, regardless of whether the traffic came from a human clicking around or from ZAP's own automation. A Context is the unit that ties a target together — a named scope (in-scope URLs by regex, tech stack hints, and, as its own section below covers, authentication, session management, and users) that every other feature attaches to.
Two things about that picture matter for everything that follows. First, the passive scanner runs continuously and automatically on every single request/response pair ZAP ever sees — including traffic you generate yourself by browsing manually through the proxy — checking headers, cookies, and page content against pattern-based rules, and it is structurally incapable of sending anything itself. Second, discovery and attack are separate concerns: the Spider (following <a href> links and HTML forms) and the AJAX Spider (driving a real headless browser via Selenium to execute JavaScript, the only way to find routes a single-page app renders client-side) both just populate the Sites tree; only the Active Scanner, reading from that tree afterward, ever sends a crafted payload. Add-ons — AJAX Spider, OpenAPI/GraphQL import, and dozens more — install from ZAP's own Marketplace, version independently of ZAP core, and carry their own quality tier (Release, Beta, Alpha), which matters more than it sounds like it should the first time an automation job references an Alpha add-on that changes behavior between releases. One more piece worth knowing exists: Zest, a JSON-based scripting language (originally a Mozilla project, donated to OWASP) for recording and replaying browser actions — the mechanism behind script-based authentication and reusable regression scripts covered further down this page.
Baseline versus full active scan — what actually changes underneath
☺ Like you're 10: Baseline only ever looks; full scan actually tries to break things — and that single difference is why one is safe almost anywhere and the other only belongs where you're explicitly allowed to attack.
A baseline scan runs the Spider for a short, time-boxed window (one minute by default, overridable) against the target, then runs the passive scanner's rules against whatever traffic that spidering produced — and nothing else. It never sends a crafted payload; it only inspects requests and responses that ordinary crawling generated. That's exactly why it's the scan most teams wire in as a required check on every pull request or merge: it finishes in minutes and is safe to point at almost any environment, staging included.
$ docker run --rm -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
zap-baseline.py -t https://staging.internal.example.com \
-r zap-baseline-report.html -J zap-baseline-report.json -I
# -I: don't fail the build on WARN-level results while a rule set is still being tunedA full (active) scan runs the same discovery step — spider, and optionally the AJAX spider for a JavaScript-heavy target — but then hands every parameter it found to the Active Scanner, which iterates through its scan policy's rules and genuinely attacks: real SQL injection strings, reflected and stored XSS payloads, path traversal sequences, command injection attempts, XXE and server-side template injection probes, each one sent and its live response inspected for evidence something worked. Two knobs tune how hard it looks: Attack Strength (Low/Medium/High/Insane — how many payload variants a rule tries) and Alert Threshold (Off/Low/Medium/High — how much evidence a rule needs before it raises a finding), set globally in a scan policy or overridden per rule ID.
$ docker run --rm -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
zap-full-scan.py -t https://staging.internal.example.com \
-j -r zap-full-report.html -J zap-full-report.json
# -j: also run the AJAX spider before the active scan startsAn active scan is a genuine attack, not a simulation of one — never point it at anything you don't have explicit authorization to test, and never at production unless that authorization specifically covers it. Active-scan payloads have no idea what an endpoint's business logic actually does: they will submit a "delete my account" form, trigger a real password-reset email flood, or place a real order, because from the scanner's point of view that endpoint is just a parameter to fuzz. Mitigate with an excludePaths entry for known-destructive routes, or point the scan at an environment seeded with disposable test data rather than trusting exclusions alone to catch everything.
The Automation Framework: driving a scan with zap.yaml
☺ Like you're 10: One YAML file replaces the whole point-and-click wizard — the entire scan plan gets written down, committed to git, diffed in review, and run the exact same way every single time.
ZAP's Automation Framework is the current, CI-native way to define a scan: a YAML plan with a top-level env block (contexts, users, authentication, session management — the whole subject of the next section) and a jobs list that runs top to bottom. Job order matters, because later jobs depend on earlier ones having populated the Sites tree: discovery jobs (spider, spiderAjax) come first, passiveScan-wait lets the continuously-running passive scanner catch up, activeScan only makes sense once there's something in the tree to attack, and report/exitStatus come last so they can act on a complete result. The packaged zap-baseline.py and zap-full-scan.py wrappers are themselves just generating an automation plan behind the scenes on recent ZAP releases — the flags you pass them are a shortcut for writing the YAML yourself, not a separate mechanism.
# zap-automation.yaml — treat the exact field names here as the pattern to look for,
# not something to copy verbatim without checking your installed version's job schema.
env:
contexts:
- name: "checkout-app"
urls: ["https://staging.internal.example.com"]
excludePaths:
- "https://staging.internal.example.com/logout.*" # never let the spider log itself out
parameters:
failOnError: true
progressToStdout: true # stream job progress to CI logs as it runs
jobs:
- type: passiveScan-config
parameters: { maxAlertsPerRule: 10 }
- type: spider
parameters:
context: "checkout-app"
user: "zap-scanner"
maxDuration: 5 # minutes — a hard cap, not a suggestion
failIfFoundUrlsLessThan: 20 # sanity check that auth actually worked
- type: spiderAjax
parameters: { context: "checkout-app", user: "zap-scanner", maxDuration: 10 }
- type: passiveScan-wait
parameters: { maxDuration: 5 }
- type: activeScan
parameters:
context: "checkout-app"
user: "zap-scanner"
maxRuleDurationInMins: 5
policyDefinition:
defaultStrength: "medium"
defaultThreshold: "medium"
rules:
- { id: 40018, threshold: "low", strength: "high" } # SQL injection matters more here
- type: report
parameters: { template: "traditional-html", reportDir: "/zap/wrk", reportFile: "zap-full-report" }
- type: exitStatus
parameters: { errorLevel: "High", warnLevel: "Medium" } # exit code drives the CI gateRun any plan the same way, whatever mix of discovery and scan jobs it contains:
$ docker run --rm -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable \
zap.sh -cmd -autorun /zap/wrk/zap-automation.yamlThe practical payoff over the older workflow — clicking through the desktop's Quick Start wizard, or hand-rolling shell scripts against the ZAP API — is the same one security as code makes for every other tool on this course: the scan plan is reviewable in a pull request, diffable when someone changes it, and identical on every run instead of depending on whoever last clicked through the GUI correctly. See Dynamic Analysis in Practice for this exact workflow wired into a full pipeline stage, alerts triaged by risk and confidence, and a companion Burp Suite Dastardly run.
Authentication and session handling — the hardest part of a real scan
☺ Like you're 10: An unauthenticated spider maps three public pages and calls it done — the rest of the application only exists once ZAP can actually log in, and stay logged in, for the entire run.
This is the single detail that most determines whether a DAST scan is testing your application or testing its login page. A Context's authentication block picks a method: form-based (a login URL plus a request body with {%username%}/{%password%} placeholders ZAP substitutes at login time), JSON-based (the same idea for a JSON login payload instead of form-encoded), script-based (a Zest or JavaScript script you write yourself, for anything with a CSRF pre-fetch step, an OAuth redirect chain, or an MFA bypass specific to a test environment), or browser-based (ZAP drives its own headless browser through the actual login form — the option that reliably survives a JavaScript-heavy SPA login that a plain HTTP request replay can't). HTTP Basic/Digest auth is handled separately, at the connection level, rather than through a Context authentication method at all.
Getting logged in once is only half the problem — session management decides whether ZAP stays logged in. Cookie-based session management is the default and needs no configuration: ZAP tracks Set-Cookie automatically. A pure API or token-based SPA has no cookie to track at all, so script-based session management takes over instead — a script that captures the token from the login response and attaches it as an Authorization: Bearer … header to every subsequent outgoing request in the context:
sessionManagement:
method: "script"
parameters:
script: "/zap/wrk/scripts/bearer-token-session.js" # attaches the captured token to every requestNeither of those matters if ZAP can't tell the difference between a live session and a dead one. Verification is the mechanism that closes that gap — a response-content check (loggedInRegex/loggedOutRegex against a marker only an authenticated page shows) or a dedicated poll strategy that hits a known authenticated-only endpoint on a schedule and checks its status:
authentication:
method: "form"
parameters:
loginPageUrl: "https://staging.internal.example.com/login"
loginRequestUrl: "https://staging.internal.example.com/login"
loginRequestBody: "username={%username%}&password={%password%}"
verification:
method: "poll"
pollUrl: "https://staging.internal.example.com/api/account"
pollFrequency: 10 # check every 10 requests, not just once at the startVerification only detects a session that died mid-scan — a CSRF token mismatch, an idle timeout, a WAF rule that flags the scanner's own traffic pattern. What actually prevents the scan from quietly degrading into an anonymous crawl for the rest of the run is Forced User Mode: a toggle that makes ZAP re-authenticate as a specific defined User on every single request in the context, rather than relying on whatever session state a request happens to carry. Turn it on before the spider starts, and a session drop three-quarters through a long active scan gets silently repaired on the very next request instead of quietly producing forty minutes of results against the public, logged-out version of the app. Credentials themselves belong in environment variables substituted at scan time, never hardcoded into the committed plan — see secrets management for the same discipline applied to a scanner's own credentials.
Day-to-day commands and interfaces
☺ Like you're 10: There's a version for clicking around by hand, a version for a pipeline that never opens a window, and a raw API underneath both of them for anything neither one covers.
Three packaged Docker scripts cover most CI needs directly — zap-baseline.py, zap-full-scan.py, and zap-api-scan.py (the last one seeds the Sites tree from an OpenAPI or GraphQL spec instead of crawling, for a target with no HTML to click through). Any scan a packaged script can't express — a specific job ordering, a custom scan policy, a script-based auth flow — goes into an automation plan and runs through zap.sh -cmd -autorun instead. Below both of those sits the REST/JSON API every front end is a client of, reachable directly with curl against a running ZAP instance (desktop, or docker run -p 8080:8080 zaproxy/zap-stable zap.sh -daemon -host 0.0.0.0 -port 8080) for anything scripted outside either wrapper:
$ curl "http://localhost:8080/JSON/spider/action/scan/?apikey=$ZAP_API_KEY&url=https://staging.internal.example.com" $ curl "http://localhost:8080/JSON/ascan/action/scan/?apikey=$ZAP_API_KEY&url=https://staging.internal.example.com" $ curl "http://localhost:8080/JSON/core/view/alerts/?apikey=$ZAP_API_KEY&baseurl=https://staging.internal.example.com"
For manual exploratory testing, the desktop application's HUD (Heads-Up Display) overlays scan controls, alert counts, and Forced User toggling directly onto the browser page you're clicking through — the tool most application security engineers actually reach for when they're not automating anything yet, just poking at a feature by hand before deciding whether it's worth writing a rule or a custom active-scan check for it.
Gotchas and failure modes
☺ Like you're 10: Most of the surprises trace back to one fact — ZAP only sees what a browser or a script would see, so anything that fools a browser fools ZAP the same way.
- A suspiciously fast, suspiciously clean scan can be a false negative, not good news. A WAF or rate limiter that starts returning 429/403 to the scanner's traffic pattern partway through produces a scan that "completes" quickly with almost no findings — because most of its requests never reached the application at all. Check the request/response status-code distribution, not just the alert count, before trusting a run that finished faster than expected.
- The traditional spider is blind to anything a single-page app renders client-side. It only follows
<a href>links and HTML forms present in the initial response; a route that only appears after JavaScript runs simply doesn't exist to it. The AJAX Spider fixes this by driving a real browser, but that realism costs real time — budget for it explicitly in a CI time box rather than discovering the difference when a pipeline stage times out. - Add-ons version independently of ZAP core. An automation job referencing a Beta or Alpha-tier add-on can change behavior — or its own YAML schema — between ZAP releases with no corresponding change in your plan. Pin the Docker image tag or digest in a pipeline that needs reproducibility, the same discipline an unpinned
--config=autoruleset needs for Semgrep. - Default Attack Strength and Alert Threshold are a compromise, not a correct setting for your app. Medium/Medium is tuned to be roughly right across every kind of application that might run it, which means it's under-tuned for some stacks and over-tuned for others. A rule that matters a lot for your app (SQL injection against a hand-rolled query layer, say) is worth lowering the threshold and raising the strength for specifically, rather than accepting the global default for every rule uniformly.
- Sizing an active scan for CI is a real capacity-planning problem, not an afterthought. A full scan against a wide application can run for hours and holds open real connections and response bodies per concurrent thread; without a
maxDurationormaxRuleDurationInMinscap, a job can silently run past the CI runner's own timeout and get killed with no report written at all, rather than failing loudly with a result you can act on.
ZAP versus Burp Suite and the rest of the DAST landscape
☺ Like you're 10: A few tools test running apps this way — the real question is rarely "which one is best," it's which one fits the job in front of you and what it's worth paying for.
| Tool | Cost / license | Automation model | Scan depth | Fits best at |
|---|---|---|---|---|
| OWASP ZAP | Free, Apache 2.0 | Automation Framework (zap.yaml) native to CI; full REST API | Solid, actively maintained rule set; community-driven pace | The default free, CI-native DAST gate every pipeline can run |
| Burp Suite Community | Free | Manual only — no automated active scan, no CI hooks | Manual testing tooling (Repeater/Intruder) only | Ad hoc manual exploration, not pipeline automation |
| Burp Suite Professional / Enterprise | Commercial, per-seat or per-scanner | Enterprise Edition adds CI/schedule automation; CLI available | Widely regarded as more refined active-scan logic and manual-testing UX | A dedicated security or pentest team, budget for a paid tool |
| Burp Suite Dastardly | Free | Fixed, non-configurable — no auth setup surface at all | Deliberately shallow; JUnit XML output | A fast, zero-friction smoke-test complement, never a replacement |
| Nuclei (ProjectDiscovery) | Free, open source | YAML templates, huge community template library | Known-CVE and misconfiguration matching, not general crawl-and-attack | Fast sweeps for specific known issues, run alongside a real DAST tool rather than instead of one |
In practice most mature pipelines run more than one: ZAP (or Burp Enterprise) as the crawl-and-attack DAST gate against staging, Dastardly as a near-instant complement wherever a full scan is too slow to fit, and something like Nuclei layered in for fast known-vulnerability sweeps that don't need a real spider at all. See Burp Suite for the fuller comparison from Burp's own side, and vulnerability management & triage for how findings from more than one scanner get deduplicated into a single backlog once you're running more than one of these in the same pipeline.
Timmy the Turtle: Baseline's green — three routes, zero passive findings. Ship it?
Foxy: Three routes? This app has forty. What did baseline actually see?
Timmy the Turtle: Only what an anonymous visitor sees. Baseline never logs in — it's the polite pass.
Ellie the Elephant: So the real run needs actual credentials in the scan config. Please tell me they're not sitting in zap.yaml in plaintext.
Timmy the Turtle: They're not — the plan pulls them from an environment variable at scan time. Same rule as everywhere else on this pipeline.
Rocky the Raccoon: Once you're actually logged in as a real user, send me in. Full active scan, every parameter, every payload — that's when I find out if the guardrail actually holds.
Professor Owl: One rule before Rocky gets his scan — only against a target you're explicitly authorized to attack. Same target, same rules, every single time.
1. What's the mechanical difference between a baseline scan and a full active scan — not just "one is safer," but what each one actually does differently under the hood? 2. Name ZAP's two discovery mechanisms and explain why a single-page app needs the second one. 3. Why is authentication verification alone not enough to keep a long active scan from silently degrading into an anonymous crawl, and what feature actually prevents it rather than just detecting it? 4. Why can a suspiciously fast, suspiciously clean ZAP scan sometimes be evidence of a problem rather than good news? 5. Give one concrete, real (non-simulated) side effect an active scan can trigger against a fragile target, and name one mitigation. 6. In one sentence each, where does ZAP sit relative to Burp Suite Professional and to Nuclei?
Check your answers
- Baseline runs the Spider for a short time-boxed window and then only runs the passive scanner's rules against whatever traffic that produced — it never sends a crafted payload. A full/active scan takes the same (or AJAX-assisted) discovery step and then hands every discovered parameter to the Active Scanner, which genuinely sends attack payloads (SQLi, XSS, path traversal, and more) and inspects the live response for evidence something worked.
- The traditional Spider (follows HTML links and forms) and the AJAX Spider (drives a real headless browser to execute JavaScript). A single-page app renders most of its routes client-side after JavaScript runs, which the traditional spider — reading only the initial HTML response — structurally cannot see.
- Verification (a
loggedInRegex/loggedOutRegexcheck, or a poll strategy) only detects that a session died mid-scan; it doesn't stop the scan from continuing on a now-anonymous session afterward. Forced User Mode is what actually prevents the degradation — it re-authenticates as a specific defined user on every single request in the context, so a session drop gets silently repaired on the very next request instead of quietly producing results against the logged-out app for the rest of the run. - Because a WAF or rate limiter that starts blocking the scanner's traffic pattern partway through produces a scan that finishes quickly with very few findings — not because the app is secure, but because most of the scanner's requests never reached the real application at all. Checking the status-code distribution, not just the alert count, is what catches this.
- For example, an active scan submitting a real "delete my account" form, triggering a real password-reset email flood, or placing a real order in an e-commerce flow — because attack payloads don't know an endpoint's business meaning, only that it's a parameter to fuzz. Mitigations include an
excludePathsentry for known-destructive routes, or scanning an environment seeded with disposable test data rather than trusting exclusions alone. - Against Burp Suite Professional: ZAP trades Burp's more refined, commercially-developed active-scan logic and manual-testing UX for being genuinely free and natively CI-automatable via the Automation Framework. Against Nuclei: ZAP is a full crawl-and-attack DAST proxy that discovers and tests routes it finds itself, while Nuclei is a fast, template-driven matcher for known CVEs and misconfigurations that doesn't crawl or reason about an application's own structure — the two are usually run alongside each other, not as substitutes.