Security Testing Milestones & Mini-Projects
In short: six projects on OWASP Juice Shop, running on your own machine. Tasks show the expected result; answer keys are folded away. The results shown were captured from Juice Shop v20.2.0 โ the app is updated often, so your version and details may differ.
Every task here is on Juice Shop (built for this) on localhost, or on the
free PortSwigger Academy. Never run these against systems you don't own or have
written permission to test.
Start Juice Shop (docker run --rm -d -p 3001:3000 --name juice bkimminich/juice-shop),
open http://localhost:3001, and find the hidden scoreboard (/#/score-board)
โ it tracks the challenges you solve. First read each milestone in the
Roadmap.
Contentsโ
- Milestone 1: Recon & baseline
- Milestone 2: Injection
- Milestone 3: Access control & auth
- Milestone 4: XSS & business logic
- Milestone 5: Scanners in CI
- Milestone 6: A findings report
- Final project
Throughout, J=http://localhost:3001.
Milestone 1: Recon & baselineโ
Practises: scoping mindset, headers, error leaks, the baseline checklist.
| # | Task | Expected result |
|---|---|---|
| 1 | Get the app version | {"version":"20.2.0"} (or newer) |
| 2 | Check security headers on / | X-Content-Type-Options and X-Frame-Options present; CSP and HSTS missing |
| 3 | Trigger an error in product search | 500 with a SQLite error message |
| 4 | Run the whole baseline checklist | Several issues noted |
| 5 | Find and open the hidden scoreboard | /#/score-board loads |
Answer key
J=http://localhost:3001
curl -s $J/rest/admin/application-version # {"version":"20.2.0"}
curl -sI $J/ | grep -iE 'content-security-policy|strict-transport|x-frame|x-content-type'
# X-Content-Type-Options: nosniff
# X-Frame-Options: SAMEORIGIN (no CSP, no HSTS)
curl -s "$J/rest/products/search?q=test')--" -w " [%{http_code}]\n" | head -c 90
# <title>Error: SQLITE_ERROR: incomplete input</title> [500]
Findings so far: missing CSP (A05), missing HSTS (A02), verbose SQL error (A05, and a hint of A03). These alone are a useful baseline report.
Try it: check the cookies Juice Shop sets (curl -sI $J/ | grep -i set-cookie).
Do they have HttpOnly, Secure, SameSite?
Milestone 2: Injectionโ
Practises: finding and confirming SQL injection.
| # | Task | Expected result |
|---|---|---|
| 1 | Log in with email ' OR 1=1-- and any password | HTTP 200; logged in as admin@juice-sh.op |
| 2 | Explain, in one sentence, why it works | The email breaks out of the SQL string |
| 3 | Confirm the search SQLi from Milestone 1 by varying the payload | Different SQL errors for different inputs |
| 4 | Write the finding for the login bypass | Steps + proof + impact + fix |
| 5 | (Bonus) PortSwigger Academy: solve the first "SQL injection" lab | Lab solved |
Answer key
J=http://localhost:3001
curl -s -X POST $J/rest/user/login -H 'Content-Type: application/json' \
-d '{"email":"'"'"' OR 1=1--","password":"anything"}' \
| python3 -c "import json,sys;a=json.load(sys.stdin)['authentication'];print('logged in as', a['umail'], '| token', len(a['token']), 'chars')"
# logged in as admin@juice-sh.op | token 717 chars
Why: the login query is built by joining strings, roughly
... WHERE email = '<input>' AND password = '...'. The input ' OR 1=1--
closes the email string, adds OR 1=1 (always true), and -- comments out the
password check โ so the first row (admin) matches. Fix: parameterised
queries, so input is only ever data.
Finding: this is the SEC-01 example in the cheat sheet โ Critical, full account takeover.
Watch out for: the shell quoting of ' OR 1=1--. The '"'"' sequence in
the examples is how you put a single quote inside a single-quoted string.
Milestone 3: Access control & authโ
Practises: BOLA/IDOR, JWT inspection, brute-force/rate-limit checks.
| # | Task | Expected result |
|---|---|---|
| 1 | With one token, read baskets 1, 2 and 3 | All return 200 โ a BOLA flaw |
| 2 | Decode the JWT payload | You see the user's email and role, base64-only (not encrypted) |
| 3 | Check GET /api/Users without a token | 401 โ correctly protected |
| 4 | Send 5 wrong logins quickly; note status and timing | No lockout (a rate-limit finding) |
| 5 | Write findings for #1 and #4; note the pass from #3 | 2 findings + 1 pass |
Answer key
J=http://localhost:3001
TOKEN=$(curl -s -X POST $J/rest/user/login -H 'Content-Type: application/json' \
-d '{"email":"'"'"' OR 1=1--","password":"x"}' | python3 -c "import json,sys;print(json.load(sys.stdin)['authentication']['token'])")
for id in 1 2 3; do curl -s -o /dev/null -w "basket $id -> %{http_code}\n" "$J/rest/basket/$id" -H "Authorization: Bearer $TOKEN"; done
# basket 1 -> 200 / basket 2 -> 200 / basket 3 -> 200 โ BOLA
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null; echo # payload: email, role, etc.
curl -s -o /dev/null -w "GET /api/Users -> %{http_code}\n" "$J/api/Users" # 401 (good)
Finding (BOLA): any authenticated user can read any basket by id โ no ownership
check. Fix: verify basket.userId == currentUser.id server-side.
Note the pass: listing all users needs a token. Report both โ coverage matters.
Try it: decode the JWT header (cut -d. -f1). What signing algorithm does
it use? What would happen if the app accepted alg: none?
Milestone 4: XSS & business logicโ
Practises: spotting reflected input, thinking about stored XSS, abusing rules.
| # | Task | Expected result |
|---|---|---|
| 1 | Send <iframe> in the search query; check the response | The tag is reflected in the JSON body |
| 2 | In the browser, try the DOM XSS challenge in the search box | Understand where output is rendered |
| 3 | Note whether a CSP would reduce the impact (from Milestone 1) | CSP is missing โ so XSS is more dangerous |
| 4 | Add an item to the basket, then change its quantity to a negative number via the API | See what happens to the total |
| 5 | Write findings; suggest fixes (output encoding + CSP; server-side validation) | 2 findings |
Answer key
J=http://localhost:3001
curl -s "$J/rest/products/search?q=%3Ciframe%3E" | grep -o "iframe" | head -1
# iframe โ reflected; whether it executes depends on how the page renders it
XSS: input is reflected; because there's no Content-Security-Policy (Milestone 1), an executing script has free rein. Fix: encode output for its context and add a restrictive CSP.
Business logic: quantities and totals must be validated on the server โ never trust a value the client can change. Juice Shop has several such challenges (tampered basket totals, applying another user's coupon).
Use only harmless proofs like alert(1). Never run real attack scripts, even
on a practice app.
Try it: in Juice Shop, post product feedback and see whether your name/comment
is shown to others unescaped (stored XSS). Use a harmless alert(1) proof only.
Milestone 5: Scanners in CIโ
Practises: DAST, SAST, dependency and secret scanning; a pipeline.
| # | Task | Expected result |
|---|---|---|
| 1 | Run a ZAP baseline (passive) scan against Juice Shop | ~88 URLs; WARNs incl. "CSP Header Not Set" |
| 2 | Clone the Juice Shop source; run npm audit | A list of dependency advisories |
| 3 | Run Trivy filesystem scan for vulns and secrets | HIGH/CRITICAL findings listed |
| 4 | Run Gitleaks on the repo | Any committed secrets reported |
| 5 | Write a security.yml workflow (SAST + deps/secrets + ZAP baseline); lint it | Passes actionlint |
Answer key
# DAST baseline (from the host, reaching the container)
docker run --rm --add-host=host.docker.internal:host-gateway ghcr.io/zaproxy/zaproxy \
zap-baseline.py -t http://host.docker.internal:3001
# Total of 88 URLs
# WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 5
# WARN-NEW: Cross-Domain Misconfiguration [10098] x 5
# ... FAIL-NEW: 0 WARN-NEW: 8 PASS: 59
The baseline confirms, automatically, the missing CSP you found by hand in Milestone 1 โ plus cross-domain misconfiguration, cacheable content and more.
The CI workflow is the one in the cheat sheet / quick reference. Gate on new HIGH/CRITICAL only.
Watch out for: an active full scan (zap-full-scan.py) sends real
attacks โ fine against your local Juice Shop, never against production or
anything you don't own.
Milestone 6: A findings reportโ
Practises: CVSS, prioritising, writing a report a team can act on.
| # | Deliverable | Expected result |
|---|---|---|
| 1 | Collect your findings from Milestones 1โ5 | 8โ12 findings |
| 2 | Rate each with CVSS + business context | Severity per finding |
| 3 | Write each in the finding template | Repro + impact + fix |
| 4 | Executive summary: top 3 risks, counts by severity | Half a page |
| 5 | Note what was correctly protected | Coverage shown |
What a strong summary looks like
Target: OWASP Juice Shop v20.2.0 (local), <date>. Authorised practice.
Summary: 11 findings โ 2 critical, 2 high, 5 medium, 2 low.
Top risks: 1) SQLi auth bypass โ full admin takeover (Critical)
2) SQLi in product search + verbose SQL errors (Critical/Medium)
3) BOLA: any user reads any basket (High)
Also: Missing CSP and HSTS; reflected input; no login rate limiting.
Protected: Listing all users requires a token (401) โ correct.
Fix first: Parameterise all queries; enforce object ownership server-side;
add CSP + HSTS; rate-limit auth endpoints.
Your numbers depend on how much you found. What matters: criticals first, each finding reproducible, fixes concrete, and passes noted.
Try it: write the one-paragraph version for a non-technical manager: what's the worst that could happen, and what are the top two fixes?
Final projectโ
Do a mini-assessment of a legal target end to end: Juice Shop challenges across all OWASP categories, or a run through several PortSwigger Academy topics (access control, SQLi, XSS, authentication).
Done when:
- Written scope (even self-authored) naming the target and allowed methods
- Findings across at least 5 OWASP categories, each reproduced with a proof
- Baseline (passive) scanner run included and triaged
- CVSS + business context per finding; no unconfirmed scanner output
- A
security.ymlCI workflow (SAST + deps/secrets + ZAP baseline) - A report with an executive summary and prioritised fixes
- Public repo (findings on a practice target only) โ proof you can deliver the security testing service