Skip to content
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after any top-up. Until Nov 30.

How it works

OWASP ZAP Basics Against Your Local Juice Shop

Run ZAP's passive baseline scan against Juice Shop on your own machine, read the report, and check one alert by hand before trusting it.

Beginner12 min readBy NotHoly

What ZAP is and where it sits

The Zed Attack Proxy, usually called ZAP, is a free tool for finding problems in web applications. It does two jobs that matter for this guide. It works as an intercepting proxy, so you can read every request your browser sends and every response it gets back. It also runs scanners that check that traffic for weak patterns.

A proxy is a middleman. Normally your browser sends a request straight to 127.0.0.1:3000. When it goes through ZAP, the browser sends the request to ZAP instead. ZAP forwards it to the application, receives the answer, stores the pair in its history, and returns the answer to the browser. The application does not know anything changed. You only get a copy of the conversation.

That placement is why the tool helps. A scanner sees what the server sends back. A proxy shows you what the browser actually did, including headers, cookies, redirects and the JavaScript the page loaded. Many web problems only show up when you compare those two views.

Passive and active scanning

ZAP has two kinds of scanning, and the difference decides what you are allowed to run and where.

Passive scanning reads traffic that already passed through ZAP. Each response is checked for things like a missing Content-Security-Policy header, cookies without the HttpOnly flag, or stack traces in error pages. The passive rules do not send attack strings. They only look at what came back.

Active scanning sends new requests built to provoke a reaction. It changes parameters, submits odd input and watches how the application responds. Most of the serious findings come from this kind of testing, and it is also the kind that behaves like an attack. Run it only against a lab you own, or against a system where you have written permission and a defined scope.

The baseline scan in this guide is passive. The ZAP documentation describes it as a script that runs the spider against the target for one minute by default, waits for the passive rules to finish, and then writes the report. It does not run attack payloads, so it is a reasonable first step on your own lab.

Why an alert is a lead, not proof

Each ZAP alert is a rule that matched. A rule can match a header that is missing, a pattern in a response body, or a combination of both. It cannot know what your application is for, which data it protects, or whether an attacker can reach the path that matters. An alert tells you where to look. It does not confirm that a weakness can be exploited.

Two kinds of error show up all the time. Some alerts are false positives: the rule matched something harmless. Other problems are missed completely, because the scanner never reached the page or never sent the input that triggers the flaw. What turns a lead into a finding is a step you can repeat yourself: a request you sent, the response you saw, and a plain explanation of why it matters. Until you have all three, the alert stays in a list marked "to verify."

Keep that rule in mind through the rest of the guide. The scanner gives you a starting point, and the verification work is yours.

Before you start

You need four things:

  • Docker. Docker Desktop on Windows or macOS works, and so does Docker Engine on Linux. Confirm it with docker version.
  • A machine you own, where you run only the lab described here.
  • A terminal. The examples use a POSIX shell. On Windows, I ran them in Git Bash.
  • A browser, if you want the optional desktop proxy step.

The lab stays on your machine. Publish its port on 127.0.0.1 only. A port published without that prefix can listen on every network interface, and Juice Shop is built to be broken.

Step 1: Start the lab

Start Juice Shop in the background with a name you can use later:

bash
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shop

The -d flag runs the container detached. I left out --rm on purpose, so the container stays around until you stop and remove it by name. The 127.0.0.1: prefix keeps the port off your network.

Check that it answers:

bash
curl -i http://127.0.0.1:3000/

On my machine the first lines of the response looked like this. I removed the progress meter that curl prints when it is not run with -s:

text
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Feature-Policy: payment 'self'
X-Recruiting: /#/jobs
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Sun, 08 Nov 2026 09:06:18 GMT
ETag: W/"2700-1a1ba6896a5"
Content-Type: text/html; charset=UTF-8
Content-Length: 9984

Your headers and dates will differ. Record the Juice Shop version too, because the application changes between releases. This endpoint returns it:

bash
curl -s http://127.0.0.1:3000/rest/admin/application-version

On my machine it printed {"version":"20.3.0"}. Put that number in your notes next to the scan results.

Step 2: Run the baseline scan

Run the scanner from the folder where you want the report to appear. This is the command for Docker Desktop:

bash
docker run --rm -t -v "$(pwd)":/zap/wrk ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.html

Each part has a job:

  • --rm removes the scanner container when the scan ends.
  • -v "$(pwd)":/zap/wrk mounts your current folder inside the container, so the report is written to your disk.
  • -t http://host.docker.internal:3000 is the target. From inside a container, host.docker.internal points at your computer, where the lab listens.
  • -r zap-report.html writes the full HTML report into the mounted folder.

On Linux, Docker Desktop's host alias does not exist by default. Add a host mapping to the same command:

bash
docker run --rm -t --add-host=host.docker.internal:host-gateway -v "$(pwd)":/zap/wrk ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.html

I tested the Docker Desktop form only. On Linux, a port published on 127.0.0.1 may not accept connections that arrive through the Docker bridge address. If the scanner reports connection errors, read the first lines of its output before changing anything else.

Two problems I hit

The first attempt failed without scanning anything. The console said A file based option has been specified but the directory '/zap/wrk' is not mounted, then printed the usage text and exited with code 3. The mount did not work in my Git Bash session, and I did not find out why. Running the command with MSYS_NO_PATHCONV=1 and the full Windows path in the -v flag fixed it:

bash
MSYS_NO_PATHCONV=1 docker run --rm -t -v "D:/path/to/your/folder:/zap/wrk" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://host.docker.internal:3000 -r zap-report.html

Replace the path with your own folder. The first run also downloads the ZAP image, and on my connection that took most of twelve minutes. Later runs start in seconds.

Reading the console output

The scanner prints one line per rule. Here is the part that matters, from my run, with the long list of passing rules shortened:

text
Using the Automation Framework
Total of 116 URLs
PASS: Vulnerable JS Library (Powered by Retire.js) [10003]
PASS: Cookie No HttpOnly Flag [10010]
...
PASS: CSP [10055]
...
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 5
WARN-NEW: Cross-Domain Misconfiguration [10098] x 5
WARN-NEW: Modern Web Application [10109] x 5
FAIL-NEW: 0	FAIL-INPROG: 0	WARN-NEW: 3	WARN-INPROG: 0	INFO: 0	IGNORE: 0	PASS: 59

WARN-NEW means a rule flagged something that the scan has not seen before. PASS means the rule ran and found nothing. The number after each rule name is ZAP's rule identifier, which you can search for in the ZAP documentation.

The command exited with code 2 in my run. The baseline documentation lists the exit codes: 0 means success, 1 means at least one FAIL, 2 means at least one WARN and no FAILs, and 3 means any other failure. A code of 2 is normal here. It only means the scanner found something to read.

Step 3: Read the report

Open zap-report.html in a browser. The summary at the top of my report showed High 0, Medium 2, Low 0 and Informational 1. Three alerts made up the whole list:

  • Content Security Policy (CSP) Header Not Set, Medium. The response does not send a Content-Security-Policy header, which lets the browser decide which scripts and frames to load.
  • Cross-Domain Misconfiguration, Medium. The response includes Access-Control-Allow-Origin: *, which allows any website to read the response in a browser.
  • Modern Web Application, Informational. ZAP noticed that the site behaves like a single-page app. This describes the application. It is not a defect.

Every alert has the same parts: a description, the URLs where it fired, the evidence ZAP saw, some extra notes, and a suggested fix. Read the evidence and the URL first. The description tells you what the rule means in general. The evidence tells you what happened in this response.

Do not rank the report by the risk label alone. ZAP assigns a risk level from its rule, not from your application's data. A Medium on a page with no sensitive data may matter less than a Low on an endpoint that handles accounts.

Step 4: Verify one alert by hand

Pick the CSP alert. Its evidence is an absence, so the test is simple: request the page, look for the header, and record the result. This command prints the number of matching lines:

bash
curl -s -i http://127.0.0.1:3000/ -o /dev/null -D - | grep -i -c '^content-security-policy'

On my machine it printed 0. The full header list I captured earlier had no Content-Security-Policy line either. The alert matches what the server sends.

Now the second alert. The evidence is the Access-Control-Allow-Origin: * line you already saw in the curl -i output. Your check should answer a different question: does this response contain anything a third-party site should not read? The lab's root page is public HTML, so the header itself is not the problem. ZAP's text makes the same point. Browsers do not let a third-party page read authenticated responses this way. The alert becomes serious only when an endpoint that returns private data also sends a wildcard origin. If you find such an endpoint, test it with your own lab account and record what it returns.

For the third alert there is nothing to prove. It describes the application type. Leave it in the report as context and move on.

Write each verified result in a table or a text file with four columns: the alert, the request you sent, what you observed, and your verdict. A verdict can be "confirmed," "not reproduced," or "needs more context." Those three answers cover most of what you will do with scanner output.

Step 5: Proxy your browser through ZAP

This step lets you watch real requests, which is the part that makes the proxy useful beyond scanning. You can do it with the desktop application or with a container running ZAP in daemon mode. I describe both. I ran the daemon version. I did not run the desktop walkthrough, so you will see no output from it below.

Desktop version (not run by me)

Install ZAP from the official site. The getting-started page links to the current installer. Open it and choose Manual Explore, which starts a browser that already sends its traffic through ZAP. Enter http://127.0.0.1:3000 and click through the home page. Then open the History tab. Each row is one request and response pair, with the method, URL and status code. Compare those rows with what you see in your own browser's developer tools. The differences show what the browser sent that you did not expect.

Daemon version (what I ran)

Start ZAP as a background proxy on port 8080:

bash
docker run -d --name zap-proxy-lab -p 127.0.0.1:8080:8080 ghcr.io/zaproxy/zaproxy:stable zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.disablekey=true -config api.addrs.addr.name=.* -config api.addrs.addr.regex=true

The api.disablekey=true option removes the API key check so the commands below work without extra setup. Keep the port published on 127.0.0.1, and remove the container when you finish. Anyone who can reach the API can control ZAP.

Confirm the API answers:

bash
curl -s http://127.0.0.1:8080/JSON/core/view/version/

Output on my machine:

text
{"version":"2.17.0"}

Send two requests through the proxy. The -x flag tells curl to use ZAP. The target name is host.docker.internal because ZAP runs inside Docker, and the name it uses for your machine is the one it resolves from there:

bash
curl -s -x http://127.0.0.1:8080 -o /dev/null -w 'proxied root http=%{http_code}\n' http://host.docker.internal:3000/
curl -s -x http://127.0.0.1:8080 -o /dev/null -w 'proxied api http=%{http_code}\n' "http://host.docker.internal:3000/rest/products/search?q=apple"

Output:

text
proxied root http=200
proxied api http=200

Ask ZAP how many messages it has recorded:

bash
curl -s "http://127.0.0.1:8080/JSON/core/view/numberOfMessages/"

On my machine it returned {"numberOfMessages":"5"}. Then list them with a short Python one-liner that prints each request line and response status line:

bash
curl -s "http://127.0.0.1:8080/JSON/core/view/messages/?start=1&count=5" | python -c "import json,sys; d=json.load(sys.stdin); [print(m['requestHeader'].splitlines()[0], '->', m['responseHeader'].splitlines()[0]) for m in d['messages']]"

Output:

text
GET http://host.docker.internal:3000/ HTTP/1.1 -> HTTP/1.1 200 OK
GET http://host.docker.internal:3000 HTTP/1.1 -> HTTP/1.0 0
GET http://host.docker.internal:3000/rest/products/search?q=apple HTTP/1.1 -> HTTP/1.1 200 OK
GET http://host.docker.internal:3000/rest HTTP/1.1 -> HTTP/1.0 0
GET http://host.docker.internal:3000/rest/products HTTP/1.1 -> HTTP/1.0 0

I sent two requests through the proxy, but the history holds five entries. Three of them have no response, which shows as status 0. I did not trace where they came from. Do not read them as evidence about Juice Shop. This is the reason to check the history yourself rather than trust a count. When your own list differs, write down what you see and ask why before you draw a conclusion.

Stop and remove the proxy when you finish:

bash
docker rm -f zap-proxy-lab

Common mistakes

Pointing the scanner at a target you do not own. The -t flag accepts any URL, and the scanner will send requests to it. Keep the target on the lab alias for your own machine.

Publishing the lab on all interfaces. -p 3000:3000 without an address listens on every interface. Always write 127.0.0.1:3000:3000.

Treating warnings as a score. A report with three warnings tells you that three rules matched. Verify each one before you count it.

Skipping the version. Juice Shop changes between releases, and so do ZAP's rules. Write down both versions with the report. The version I used was Juice Shop 20.3.0 and ZAP 2.17.0.

Leaving the proxy running. The daemon has its API key disabled. Remove the container after each session.

Reading the status column as proof. A row with status 0 means no response was recorded. It is not a finding.

Mounting the wrong folder. If the report is missing, check the -v mount first. The scanner can run successfully and still write nothing where you expect.

How to go further

Keep one notes file for the lab. Each line should hold one alert, the request that tested it, what you saw, and your verdict. After a few scans, the file becomes a record of what the tool found, what you confirmed, and what it got wrong.

Read the Juice Shop setup guide if you skipped it, since this article assumes the same lab. The next article uses the same lab to explain SQL injection. That is a better place to see why a scanner alert needs a manual check: the flaw is about how input reaches a query, and a tool can only hint at that.

The ZAP documentation covers contexts, authentication and active scanning. Those topics matter once you can tell a confirmed issue from a rule match. Learn them against the lab, with permission, and do not carry them to other systems.

The Juice Shop project keeps its challenge list and setup notes in its GitHub repository. Check its releases page for the current version before you pull the image.

Sources

Sources

  1. ZAP documentation
  2. ZAP Baseline Scan (Docker)
  3. ZAP getting started
  4. OWASP Juice Shop (GitHub)

More in Security tools

Security tools

Nmap Basics on Your Own Lab

Nmap maps the open ports on your own machine. Learn connect and SYN scans, service detection and output files, then match each port to its process.

Beginner12 min read