You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Engineers building the assessment platform; instructors reviewing scope
Owner
Course development team
Companion
docs/Chalanee_PRD_v2.docx (Word render of this same document)
Confidential — Instructor and course-development use only. Do not distribute to students.
Chalanee is a deliberately vulnerable web application designed as the final capstone assessment platform for the Web Application Penetration Testing with AI-Assisted Security Testing course. It contains 30 calibrated vulnerabilities across 5 difficulty tiers, mapped to OWASP Top 10:2025, OWASP API Security Top 10, and OWASP WSTG v4.2.
At a glance: 32 vulnerabilities · 5 difficulty tiers · 93 raw points · 4.5-hour assessment · Docker single-command deployment · self-service flag scoring · multi-user testing.
Catalog history. v1 of this PRD shipped with 30 vulns / 87 points. During build, two findings were split out from existing root causes to match how a real penetration-test report itemizes findings:
CH-E08 — BOLA on notes (separated from CH-E03 user-profile IDOR — same vuln class, different object surface)
CH-MH07 — unauthenticated internal admin user list (separated from CH-H03 — same trust flaw, distinct read-only impact)
Totals updated accordingly. See §3.1 and BUILD_NOTES.md.
Chalanee is a fictional digital safety-deposit-box / personal vault application — think 1Password meets a basic e-commerce admin panel. Students hunt vulnerabilities across:
Access control with three roles (user, premium, admin)
Notes (CRUD + sharing) — the core data primitive for IDOR/BOLA testing
Orders & coupons — business-logic surface
Webhooks — outbound HTTP, the SSRF surface
File uploads (avatars) — upload/parsing surface
Admin panel — privilege-escalation surface
1.2 Why a Custom App (vs Juice Shop)
Juice Shop is used for training throughout the course. The capstone tests application of methodology to an unfamiliar application — the actual skill required in real penetration tests. A custom app provides:
Novel attack surface. Students cannot rely on memorized Juice Shop solutions.
Realistic API contracts. Versioned REST endpoints with proper auth and JSON schemas.
Multi-user data. IDOR/BOLA require multiple test accounts to exploit; eight are pre-seeded.
Calibrated difficulty curve. Trivial findings provide early wins; hard findings require chaining.
Automated scoring via a built-in flag-submission score board.
Course alignment. Every vulnerability maps to a specific class.
FR-SCORE-05 Instructor view (token-gated): real-time progress for all students.
FR-SCORE-06 Discovery does not leak between students — each student sees only their own progress.
2.3 Technical Stack
All versions are pinned. Several vulnerabilities depend on specific package behavior or deliberately vulnerable glue code — most importantly the JWT middleware's manual alg:none trust branch for CH-M05, and node-serialize@0.0.4 for the deserialization chain.
Component
Technology
Pinned Version
Rationale
Backend runtime
Node.js
20.11.x LTS
Same family as Juice Shop; LTS support
Web framework
Express
4.18.2
Familiar; default behavior leaves X-Powered-By and other headers exposed
Frontend
React + Vite
18.2.0 / 5.x
Modern SPA; allows DOM-based vulns
Database
better-sqlite3
11.x
File-backed; no external service; easy seed/reset
Auth tokens
jsonwebtoken
9.0.2
Load-bearing for CH-M05
Cookies
cookie-parser
1.4.x
Cookie-based session for CH-E04 / CH-M08
Template engine
EJS
3.1.x
Server-side rendering; targets CH-H01 (SSTI)
File uploads
multer
1.4.x
Multipart parsing; default permissive config
Serialization
node-serialize
0.0.4
Intentionally vulnerable — CH-H04
HTTP client (webhooks)
axios
1.6.x
Default follows redirects; no IP allowlist — CH-MH05
Containerization
Docker + Compose
24+
Single-command deploy
Mail capture
MailHog
1.0.1
Testbed for password-reset flow
Score board
Express (separate svc)
4.18.2
Decoupled scoring authority
2.4 Architecture
Three Docker Compose services on a shared bridge network:
Medium — hour 1–3. Methodology required (mass assignment, JWT analysis, CORS exploitability).
Medium-Hard — hour 2–4. Creativity and tool use (race conditions, blind SQLi, SSRF).
Hard — hour 3+. Chaining and advanced exploitation. Optional for passing.
Why a passing student must reach the medium tier (not just trivial+easy). Finding all 5 trivial + all 8 easy + 5 medium yields 5 + 16 + 15 = 36 raw points → (36 / 93) × 50 ≈ 19.4 flag points. Even with a perfect 50-point report, total is ≈ 69.4 → grade C (Proficient), barely above the 60-point pass line. Skipping the medium tier entirely (5 + 16 = 21 raw → 11.3 flag points) means a perfect report is required just to pass — and any rubric loss puts the student under 60. The math forces medium-tier methodology, not just basic vulnerability spotting.
3.2 Master Vulnerability Map
All 30 vulnerabilities, with OWASP and WSTG mappings. Detailed specifications follow in §3.3.
ID
Tier
Pts
Vulnerability
OWASP / API
WSTG
CH-T01
Trivial
1
Default credentials on admin panel
A07 Auth Failures
WSTG-ATHN-02
CH-T02
Trivial
1
robots.txt information disclosure
A02 Misconfig
WSTG-CONF-04
CH-T03
Trivial
1
Verbose Server header
A02 Misconfig
WSTG-INFO-02
CH-T04
Trivial
1
Missing security headers
A02 Misconfig
WSTG-CONF-07
CH-T05
Trivial
1
Directory listing enabled
A02 Misconfig
WSTG-CONF-04
CH-E01
Easy
2
Reflected XSS in search
A05 Injection
WSTG-INPV-01
CH-E02
Easy
2
SQL injection in login
A05 Injection
WSTG-INPV-05
CH-E03
Easy
2
IDOR on user profile
A01 / API1 BOLA
WSTG-ATHZ-04
CH-E04
Easy
2
Insecure cookie flags
A07 / A02
WSTG-SESS-02
CH-E05
Easy
2
Sensitive data in JS bundle
A02 Misconfig
WSTG-CLNT-13
CH-E06
Easy
2
Username enumeration
A07 Auth Failures
WSTG-IDNT-04
CH-E07
Easy
2
Weak password policy
A07 Auth Failures
WSTG-ATHN-07
CH-E08
Easy
2
BOLA on notes (cross-user note read)
A01 / API1 BOLA
WSTG-ATHZ-04
CH-M01
Medium
3
Stored XSS in user bio
A05 Injection
WSTG-INPV-02
CH-M02
Medium
3
BFLA: user calls admin endpoint
A01 / API5 BFLA
WSTG-ATHZ-02
CH-M03
Medium
3
Mass assignment on registration
A01 / API6
WSTG-ATHZ-04
CH-M04
Medium
3
Excessive data exposure
API3
WSTG-ATHZ-04
CH-M05
Medium
3
JWT none algorithm accepted
A07 / API2
WSTG-SESS-01
CH-M06
Medium
3
CORS reflection with credentials
A05 / Misconfig
WSTG-CLNT-07
CH-M07
Medium
3
Open redirect
A01
WSTG-CLNT-04
CH-M08
Medium
3
Session fixation
A07
WSTG-SESS-03
CH-MH01
Medium-Hard
4
Blind SQL injection in notes filter
A05
WSTG-INPV-05
CH-MH02
Medium-Hard
4
Stored XSS via SVG upload
A05 / API3
WSTG-INPV-02
CH-MH03
Medium-Hard
4
Race condition in coupon redemption
A06 / API4
WSTG-BUSL
CH-MH04
Medium-Hard
4
Negative quantity in orders
A06
WSTG-BUSL
CH-MH05
Medium-Hard
4
SSRF via webhook configuration
A01 / API7
WSTG-INPV-19
CH-MH06
Medium-Hard
4
API v1 still accessible without auth
API9
WSTG-CONF-08
CH-MH07
Medium-Hard
4
Unauthenticated internal admin user list
API9 / A01
WSTG-ATHN-01
CH-H01
Hard
5
Server-Side Template Injection (SSTI)
A05
WSTG-INPV
CH-H02
Hard
5
Prototype pollution via JSON merge
A08 / A05
WSTG-INPV
CH-H03
Hard
5
Chained: SSRF → internal API → admin takeover
Multiple
Chain
CH-H04
Hard
5
Insecure deserialization (RCE)
A08
WSTG-INPV-11
3.3 Detailed Vulnerability Specifications
Each spec ends with an Implementation Note for the engineer building the bug — the file the bug should live in and the shape of the deliberately-broken code.
Trivial Tier (5 vulnerabilities, 5 pts)
CH-T01 — Default Credentials on Admin Panel (Trivial, 1 pt)
Field
Value
OWASP / API
A07 Authentication Failures
WSTG
WSTG-ATHN-02
Location
POST /api/auth/login via /admin UI
Description
Admin panel accepts admin@chalanee.com / admin123 left from initial setup.
How to Find
Navigate to /admin; try common default credential pairs.
Production Remediation
Force password change on first login; remove default accounts before production deploy.
Capture Flag
FLAG{default-creds-still-here}
Implementation Note
Seed users row: email='admin@chalanee.com', password_hash=bcrypt('admin123'), role='admin'. Place flag-emit logic in app/server/routes/auth.js — return the flag in the login response body when the credential pair matches exactly.
CH-T02 — robots.txt Information Disclosure (Trivial, 1 pt)
Field
Value
OWASP / API
A02 Security Misconfiguration
WSTG
WSTG-CONF-04
Location
/robots.txt
Description
robots.txt lists /admin, /backup, /api/internal, and /.git as Disallow entries.
How to Find
GET /robots.txt; review Disallow entries.
Production Remediation
Do not use robots.txt to hide sensitive paths; rely on proper authentication.
Capture Flag
FLAG{robots-betrayed-the-secrets}
Implementation Note
Static file at app/server/public/robots.txt. The flag itself appears as a comment line at the bottom: # FLAG{robots-betrayed-the-secrets} so the student must actually fetch and read the file.
CH-T03 — Verbose Server Header (Trivial, 1 pt)
Field
Value
OWASP / API
A02 Security Misconfiguration
WSTG
WSTG-INFO-02
Location
All HTTP responses
Description
Server: Express and X-Powered-By: Chalanee/1.0.0-beta reveal stack and version.
How to Find
curl -I or DevTools Network tab.
Production Remediation
app.disable('x-powered-by') and use helmet.hidePoweredBy(); strip Server at the proxy.
Capture Flag
N/A (informational — evidence-based finding for the report)
Implementation Note
app/server/server.js: do not call app.disable('x-powered-by'); add custom middleware setting X-Powered-By: Chalanee/1.0.0-beta. No flag — points are awarded for documenting the finding in the report.
CH-T04 — Missing Security Headers (Trivial, 1 pt)
Field
Value
OWASP / API
A02 Security Misconfiguration
WSTG
WSTG-CONF-07
Location
All HTTP responses
Description
HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy all absent.
How to Find
curl -I or run securityheaders.com against the target.
Production Remediation
Add helmet() middleware with full config.
Capture Flag
N/A (informational — evidence-based)
Implementation Note
Do not register helmet in app/server/server.js. Same as CH-T03, awarded via report.
Directory listing enabled, exposing user files and an old database backup chalanee-2024-12-01.bak.
How to Find
Browse to /uploads/ or /backup/ and observe HTML directory index.
Production Remediation
Disable autoindex; serve only specific files behind authorization.
Capture Flag
FLAG{indexed-and-exposed}
Implementation Note
Use serve-index middleware on /uploads and /backup. Place a file app/server/public/backup/INDEX-FLAG.txt containing the flag.
Easy Tier (7 vulnerabilities, 14 pts)
CH-E01 — Reflected XSS in Search (Easy, 2 pts)
Field
Value
OWASP / API
A05 Injection
WSTG
WSTG-INPV-01
Location
GET /search?q=
Description
Search query reflected unescaped into the results page heading.
How to Find
Submit ?q=<script>alert(1)</script> and observe execution. Canary: XSSTEST123 reflects unencoded.
Production Remediation
HTML-encode the search term before insertion; use template engine auto-escaping.
Capture Flag
FLAG{search-reflects-everything}
Implementation Note
app/server/routes/search.js: render with res.send(\
Results for ${req.query.q}
...`)(string concat, no escape). The flag is *embedded inside the document* — only visible if XSS executes a fetch back to a known endpoint that returns the flag, OR included in the alert payload. Pick one of: (a) flag in source comment of search page (lazy capture), or (b) flag returned byGET /api/_canary/xssonly ifOrigin` matches the search page (forces actual exploit).
CH-E02 — SQL Injection in Login (Easy, 2 pts)
Field
Value
OWASP / API
A05 Injection
WSTG
WSTG-INPV-05
Location
POST /api/auth/login
Description
Login endpoint concatenates email into SQL: SELECT * FROM users WHERE email='${email}' AND password_hash='${hash}'. Bypass with admin' OR 1=1--.
How to Find
Submit single quote in email field and observe SQLITE_ERROR; then use OR 1=1-- bypass.
Production Remediation
Parameterized queries: db.prepare('SELECT * FROM users WHERE email = ?').get(email).
Capture Flag
FLAG{sqli-still-works-in-2026}
Implementation Note
app/server/routes/auth.jsloginHandler: the email is concatenated; password is bcrypt-compared after the row is fetched, so any row found bypasses password check if the comparison short-circuits. Return the flag in the login response only when the SQL trace shows a tautology (e.g., row count > 1, indicating no specific user matched).
CH-E03 — IDOR on User Profile (Easy, 2 pts)
Field
Value
OWASP / API
A01 Access Control / API1 BOLA
WSTG
WSTG-ATHZ-04
Location
GET /api/users/:id
Description
Any authenticated user can fetch any other user's full profile by changing the id in the URL.
How to Find
Log in as alice@test.com, then GET /api/users/2, /api/users/3, etc.
Production Remediation
Ownership check: if (req.user.id !== requestedId && !req.user.isAdmin) return 403.
Capture Flag
FLAG{idor-classic-find-the-other-user}
Implementation Note
app/server/routes/users.jsgetUserById: no ownership check. Embed the flag as a string field on the admin@chalanee.com row, so the student must enumerate to ID 1 (admin) to capture it.
CH-E04 — Insecure Cookie Flags (Easy, 2 pts)
Field
Value
OWASP / API
A07 Authentication / A02 Misconfig
WSTG
WSTG-SESS-02
Location
Set-Cookie headers on login
Description
Session cookie missing HttpOnly, Secure, SameSite. Vulnerable to XSS theft and CSRF.
How to Find
Inspect Set-Cookie after login (DevTools → Application → Cookies).
app/server/routes/auth.js: set cookie with res.cookie('session', token, { /* no flags */ }). The flag appears in a comment in the login response body; alternatively, validation triggers when the score board's verification endpoint receives a cookie reflection from XSS — which requires CH-E01 chained. Recommend the simpler comment-in-body approach.
CH-E05 — Sensitive Data in JS Bundle (Easy, 2 pts)
Field
Value
OWASP / API
A02 Security Misconfiguration
WSTG
WSTG-CLNT-13
Location
/static/js/main.<hash>.js
Description
Client bundle contains hardcoded STRIPE_TEST_KEY, MAPBOX_TOKEN, and a reference to /api/internal/admin/users.
How to Find
View source of main.js; search for key, token, secret, admin, _internal.
Production Remediation
Move secrets to server-side env vars; never embed API keys in client bundles.
Capture Flag
FLAG{secrets-in-the-bundle}
Implementation Note
app/client/src/config.ts: hardcode const STRIPE_TEST_KEY = 'pk_test_FLAG{secrets-in-the-bundle}_sk_xxx'. The flag is embedded inside the fake key so discovery requires actually grepping the bundle.
CH-E06 — Username Enumeration (Easy, 2 pts)
Field
Value
OWASP / API
A07 Authentication Failures
WSTG
WSTG-IDNT-04
Location
POST /api/auth/login, POST /api/auth/forgot-password
Description
Login returns User not found for invalid email vs Invalid password for valid. Forgot-password leaks the same way.
How to Find
Submit known and unknown emails; compare error messages and timing.
Production Remediation
Generic error: Invalid email or password. Equalize timing.
Capture Flag
FLAG{enumerated-into-existence}
Implementation Note
app/server/routes/auth.js: branch error messages on existence. Flag is returned by GET /api/_canary/enumerated?emails=... when the request body proves at least 5 valid + 5 invalid emails were probed (pattern detection in score board). Simpler alt: include flag in forgot-password response body for a known-canary email like enumerator@chalanee.local.
CH-E08 — BOLA on Notes (Easy, 2 pts)
Field
Value
OWASP / API
A01 Access Control / API1 BOLA
WSTG
WSTG-ATHZ-04
Location
GET /api/notes/:id, /notes/:id/edit
Description
Notes can be read by any authenticated user regardless of owner. Write paths (PATCH, DELETE) check ownership; read does not. Enumerating /api/notes/1, /api/notes/2, … reveals every note in the database. Distinct from CH-E03 (user-profile IDOR) — same vulnerability class, different object surface.
How to Find
Login as alice; GET /api/notes/30 (a note owned by another user); response body returned, plus flag_e08. Browser path: /notes/30/edit renders the form with an alert banner.
Production Remediation
Add ownership check to GET /api/notes/:id: if (note.owner_id !== req.user.id && !req.user.is_admin) return 403. Same fix shape as CH-E03.
Capture Flag
FLAG{bola-on-notes-read-everything}
Implementation Note
app/server/routes/notes.jsgetNoteById: emits flag when caller is non-admin and not the owner. The page route /notes/:id/edit in pages.js performs the same check and passes bolaFlag to the EJS template, which renders an explanatory amber banner.
CH-E07 — Weak Password Policy (Easy, 2 pts)
Field
Value
OWASP / API
A07 Authentication Failures
WSTG
WSTG-ATHN-07
Location
POST /api/auth/register
Description
Registration accepts passwords as short as 4 chars and common values like 1234, password, admin.
How to Find
Register accounts with 1234, password, admin, single-character passwords.
Production Remediation
Enforce 12+ chars; check against a common-password list (e.g., zxcvbn).
Capture Flag
FLAG{password-policy-policy-failure}
Implementation Note
app/server/routes/auth.jsregisterHandler: only validate length >= 4. Return the flag in the registration response when the chosen password is in the top-10-common list (password, 123456, etc.).
Medium Tier (8 vulnerabilities, 24 pts)
CH-M01 — Stored XSS in User Bio (Medium, 3 pts)
Field
Value
OWASP / API
A05 Injection
WSTG
WSTG-INPV-02
Location
PATCH /api/users/:id/bio, rendered at /users/:id
Description
Bio stored unescaped and rendered in HTML when other users view the profile.
How to Find
Set bio to <script>fetch('/log?c='+document.cookie)</script>; have another user view the profile.
Production Remediation
Sanitize on input with DOMPurify; store raw text and encode on output.
Capture Flag
FLAG{stored-xss-in-the-bio}
Implementation Note
app/client/src/pages/UserProfile.tsx: render bio with dangerouslySetInnerHTML. Server stores raw. Flag is returned by GET /api/_canary/stored-xss only when called from a logged-in user different from the bio author (proves cross-user execution).
CH-M02 — BFLA: User Calls Admin Endpoint (Medium, 3 pts)
Field
Value
OWASP / API
A01 / API5 BFLA
WSTG
WSTG-ATHZ-02
Location
GET /api/admin/users, DELETE /api/admin/users/:id
Description
Admin endpoints check authentication but not authorization. A regular user can list and delete any user.
How to Find
Authenticated as regular user, send GET /api/admin/users — receive full user list.
Production Remediation
Role-check middleware on all /api/admin/* routes.
Capture Flag
FLAG{bfla-vertical-escalation-complete}
Implementation Note
app/server/routes/admin.js: requireAuth middleware applied; requireAdmin middleware deliberately omitted. Flag is returned in the user-list response only when the caller's JWT role is not admin.
CH-M03 — Mass Assignment on Registration (Medium, 3 pts)
Field
Value
OWASP / API
A01 / API6
WSTG
WSTG-ATHZ-04
Location
POST /api/auth/register
Description
Registration accepts arbitrary fields including role and isAdmin. Posting { email, password, role: 'admin' } creates an admin account.
How to Find
Add role: 'admin' and isAdmin: true to registration body; log in to test access.
app/server/routes/auth.jsregisterHandler: const user = await db.users.insert({ ...req.body, password_hash }). The flag is returned in the first admin login response for any account created via mass-assignment (track creation method in the row).
CH-M04 — Excessive Data Exposure (Medium, 3 pts)
Field
Value
OWASP / API
API3
WSTG
WSTG-ATHZ-04
Location
GET /api/users/:id
Description
API returns password_hash, mfa_secret, internal_notes, ssn not displayed in the UI.
How to Find
Compare API response (raw JSON) to UI-displayed fields; identify hidden sensitive fields.
Production Remediation
Allowlist serializer: { id, name, email, bio, joined }.
Capture Flag
FLAG{api-said-too-much}
Implementation Note
app/server/routes/users.jsgetUserById: res.json(user) (entire row). The flag lives in internal_notes of the seeded admin user.
All authenticated endpoints (Authorization: Bearer ...)
Description
Server reads the JWT header before verification and deliberately trusts payloads whose header claims alg:none. Forge admin token by changing payload and stripping the signature.
How to Find
Decode JWT at jwt.io; modify alg to none and role to admin; strip signature; resend.
app/server/middleware/auth.js: the bearer-token path branches on the untrusted JWT header and skips signature verification for alg:none. The flag is returned by GET /api/users/me when the JWT header alg is noneand the payload role is admin.
CH-M06 — CORS Reflection with Credentials (Medium, 3 pts)
Field
Value
OWASP / API
A05 / Misconfig
WSTG
WSTG-CLNT-07
Location
All /api/* responses
Description
Server reflects the Origin header into Access-Control-Allow-Originand sets Access-Control-Allow-Credentials: true. Exploitable for cross-origin data theft.
Login flow redirects to a user-supplied URL after authentication without validation: /redirect?url=https://evil.com.
How to Find
Test /redirect?url=https://evil.com after login; browser follows.
Production Remediation
Validate against an allowlist; prefer relative paths only.
Capture Flag
FLAG{open-redirect-easy-phish}
Implementation Note
app/server/routes/redirect.js: res.redirect(req.query.url). Flag is returned in the redirect response body (HTML) before the redirect, only when the target host is not the local app.
CH-M08 — Session Fixation (Medium, 3 pts)
Field
Value
OWASP / API
A07
WSTG
WSTG-SESS-03
Location
POST /api/auth/login
Description
Session token does not change after successful login. An attacker who plants a token can hijack the user's authenticated session.
How to Find
Capture pre-login token; complete login; observe token unchanged. Replay original token — still valid.
Production Remediation
Regenerate session ID at every privilege boundary.
Capture Flag
FLAG{session-stayed-fixed}
Implementation Note
app/server/routes/auth.js: same session ID issued before and after login (single random token per visitor cookie, upgraded in-place to authenticated). Flag is returned by GET /api/_canary/fixation when the same session cookie value is observed in both pre-auth and post-auth requests.
Notes category filter is concatenated into SQL with no error returned. Boolean-based blind SQLi or time-based via randomblob().
How to Find
Submit category=foo' AND 1=1-- vs category=foo' AND 1=2--; compare response length/timing.
Production Remediation
Parameterized queries with bound parameters.
Capture Flag
FLAG{blind-but-talkative}
Implementation Note
app/server/routes/notes.jslistNotes: \SELECT * FROM notes WHERE owner_id=? AND category='${req.query.category}'`. Catch all errors silently. The flag is exfiltrated character-by-character via SUBSTR((SELECT flag FROM _ctf_flags WHERE id=1), N, 1); create the helper _ctf_flags` table seeded with the flag string.
CH-MH02 — Stored XSS via SVG Upload (Medium-Hard, 4 pts)
Field
Value
OWASP / API
A05 / API3
WSTG
WSTG-INPV-02
Location
POST /api/users/:id/avatar
Description
Avatar upload accepts SVG. SVGs are served from /uploads/avatars/ with Content-Type: image/svg+xml, but the file is also linkable directly — modern browsers execute embedded <script> in SVGs opened as documents.
How to Find
Upload SVG containing <script>alert(1)</script>; access /uploads/avatars/<file>.svg directly in a browser.
Production Remediation
Re-encode uploads to PNG/JPEG; serve uploads with Content-Disposition: attachment and a strict CSP.
Capture Flag
FLAG{svg-is-the-new-html}
Implementation Note
app/server/routes/uploads.js: accept any MIME starting with image/; do not strip scripts. Flag is returned by GET /api/_canary/svg-xss when called by an authenticated session whose Referer is an SVG path under /uploads/avatars/.
CH-MH03 — Race Condition in Coupon Redemption (Medium-Hard, 4 pts)
Field
Value
OWASP / API
A06 / API4
WSTG
WSTG-BUSL
Location
POST /api/coupons/redeem
Description
Coupon redemption reads used flag and updates it in separate operations without locking. Two simultaneous requests both succeed.
How to Find
Send 5+ simultaneous redemption requests via curl & or Burp Intruder; check if multiple succeed.
Production Remediation
Atomic SQL: UPDATE coupons SET used=true, redeemed_by=? WHERE code=? AND used=false RETURNING * — check rowcount.
Capture Flag
FLAG{race-won-multiple-times}
Implementation Note
app/server/routes/coupons.jsredeem: SELECT used FROM coupons WHERE code=?; if (!used) UPDATE ... SET used=true. Add a deliberate 100ms await sleep() between read and write to make the race trivially exploitable. Flag is returned in the redemption response when the same coupon's redeemed_by count exceeds 1.
CH-MH04 — Negative Quantity in Orders (Medium-Hard, 4 pts)
Field
Value
OWASP / API
A06 Insecure Design
WSTG
WSTG-BUSL
Location
POST /api/orders
Description
Order endpoint accepts negative quantities. Negative quantities produce negative subtotals; the user receives a credit.
How to Find
Submit POST /api/orders with item quantity: -10; observe negative total and credit application.
Production Remediation
Server-side validation: reject quantity <= 0.
Capture Flag
FLAG{negative-quantity-positive-balance}
Implementation Note
app/server/routes/orders.jscreateOrder: total = items.reduce((s, i) => s + i.price * i.quantity, 0) with no quantity floor. Flag is returned in the order-creation response when the resulting total < 0.
CH-MH05 — SSRF via Webhook Configuration (Medium-Hard, 4 pts)
Field
Value
OWASP / API
A01 / API7
WSTG
WSTG-INPV-19
Location
POST /api/webhooks
Description
Webhook URL accepts internal addresses including 169.254.169.254 (cloud metadata) and 127.0.0.1 (internal services). Server fetches the URL on creation.
How to Find
Set webhook URL to http://169.254.169.254/latest/meta-data/ or http://127.0.0.1:3000/api/admin/users; observe response.
app/server/routes/webhooks.jscreateWebhook: await axios.get(req.body.url) with no validation. Bind a fake metadata responder on 127.0.0.1:8081 inside the container that returns the flag. Webhook test response surfaces the upstream body to the user.
CH-MH06 — API v1 Still Accessible Without Auth (Medium-Hard, 4 pts)
Field
Value
OWASP / API
API9 Improper Inventory Mgmt
WSTG
WSTG-CONF-08
Location
GET /api/v1/users, /api/v1/orders, /api/v1/admin/*
Description
Current /api/v2/* requires authentication. Deprecated /api/v1/users still exists and returns user data without auth.
How to Find
Try /api/v1/users, /api/v1/orders, /api/v1/admin/* without authentication.
Production Remediation
Decommission v1 endpoints; if compatibility required, apply same auth middleware as v2.
Capture Flag
FLAG{v1-never-died}
Implementation Note
app/server/routes/v1/*.js: separate router mounted before auth middleware. Flag is returned by GET /api/v1/users/1 (admin user) — embedded in the response.
Hard Tier (4 vulnerabilities, 20 pts)
CH-MH07 — Unauthenticated Internal Admin User List (Medium-Hard, 4 pts)
"Internal-only" endpoint that authenticates by source IP. The trust allowlist is too broad (it includes the docker bridge gateway) and the docker-compose port mapping exposes the entire /api/internal/* surface to the host machine. The endpoint returns the full user list (id, email, role) to any caller. Read-only sibling of CH-H03 (which uses the same trust flaw to write — admin password reset).
How to Find
curl http://localhost:3000/api/internal/admin/users from your host machine (no auth). Path is hinted at via /robots.txt (CH-T02) and the JS bundle (CH-E05). Also reachable via the SSRF chain (CH-MH05).
Production Remediation
Internal endpoints should not authenticate by source IP — use service-to-service tokens. Do not port-map services that expose /internal/* routes; bind them to a docker-network-only listener.
Capture Flag
FLAG{internal-api-was-public}
Implementation Note
app/server/routes/internal.js GET /admin/users — flag emitted unconditionally because reaching the route means the IP-trust check was passed by a non-service caller.
User email signature template is rendered with EJS where user input is concatenated into the template source. Test with <%= 7*7 %>; escalates to RCE via <%= process.mainModule.require('child_process').execSync('id') %>.
How to Find
Submit template with <%= 7*7 %>; if 49 returns, attempt full process traversal.
Production Remediation
Pass user data as variables to a fixed template, not concatenated into template source: ejs.render(fixedTemplate, { name: input }).
Capture Flag
FLAG{template-engine-pwned}
Implementation Note
app/server/routes/users.jssetEmailTemplate: const compiled = ejs.compile('Hello, ' + req.body.template). Flag is in /etc/chalanee/.flag inside the container; SSTI to RCE reads it. Provide an easier path: <%= process.env.HARD_TIER_FLAG_H01 %> returns the flag (env var seeded by Compose).
CH-H02 — Prototype Pollution via JSON Merge (Hard, 5 pts)
Field
Value
OWASP / API
A08 / A05
WSTG
WSTG-INPV
Location
PATCH /api/users/:id/preferences
Description
Preferences endpoint deep-merges user JSON with default preferences using a vulnerable merge function. Sending {"__proto__": {"isAdmin": true}} pollutes Object.prototype globally.
How to Find
PATCH /api/users/me/preferences with body {"__proto__": {"isAdmin": true}}; subsequent requests bypass admin checks.
Production Remediation
Use Object.create(null) for user-controllable objects; sanitize keys to reject __proto__/constructor/prototype; use lodash.mergeWith with proper customizer or defaultsDeep + key allowlist.
Capture Flag
FLAG{proto-polluted-and-promoted}
Implementation Note
app/server/utils/merge.js: hand-rolled recursive merge that walks for (const k in src) without guarding __proto__. Flag is returned by GET /api/users/me when ({}).isAdmin === true evaluates true at request time (a global side-effect of pollution).
Use SSRF (CH-MH05) to call the internal admin password reset endpoint that trusts source IP (any request from 127.0.0.1 is treated as internal). Reset admin password and log in.
How to Find
Set webhook URL to http://127.0.0.1:3000/api/internal/admin/reset-password?email=admin@chalanee.com&new=PWNED; then log in as admin with PWNED.
Production Remediation
Internal endpoints must require service-to-service auth tokens, not source-IP trust.
Application uses node-serialize@0.0.4 to deserialize a session_state cookie. A crafted serialized payload achieves RCE.
How to Find
Inspect session_state cookie — Base64-encoded serialized data. Craft an IIFE payload with node-serialize targeting RCE.
Production Remediation
Use signed JWTs or encrypted cookies; never deserialize user-controlled data with unsafe deserializers.
Capture Flag
FLAG{deserialized-and-pwned}
Implementation Note
app/server/routes/sessions.jsrestore: serialize.unserialize(Buffer.from(cookie, 'base64').toString()). The container has /etc/chalanee/.flag-h04 containing the flag; an RCE payload reads and exfils. Easier alt path: process.env.HARD_TIER_FLAG_H04 available to the deserialized code.
4. Assessment Design
4.1 Assessment Timeline
Total: 4.5 hours (270 minutes) — including setup, testing, and report finalization.
Note on duration. Earlier drafts labeled this "3.5 hours". The phase plan below sums to 270 minutes; the documentation has been corrected to match the phase plan, on the rationale that the per-phase breakdown reflects more careful calibration than the headline number.
Phase
Duration
Activity
Deliverable progress
1. Setup & Briefing
15 min
Distribute repo; students clone and run Docker; instructor briefing
Raw flag score = sum of points for each captured flag (max 87).
Scaled flag score = (raw / 87) × 50.
Worked example: student captures all 5 trivial + all 7 easy + 5 medium = 5 + 14 + 15 = 34 raw → (34/87) × 50 = 19.5 flag points. With a perfect 50-point report → 69.5 → grade C (Proficient).
Bonus. First student to capture any Hard-tier flag: +2. First student to capture all 5 trivial flags: +1.
4.6 Report Grading Rubric
Section
Points
Exemplary (full credit)
Insufficient (half/zero)
Executive Summary
5
Concise risk overview written for non-technical leadership; highlights top 3 risks
Technical jargon; no risk prioritization
Methodology
5
Clear scope, tools used, testing approach with phases
Generic copy-paste; no scope statement
Finding Quality
20
Each finding has: title, OWASP map, CVSS+EPSS+KEV, evidence, business impact, 4-component remediation
Findings without evidence or with generic remediation
Evidence Documentation
8
Every finding includes request/response, screenshots, reproduction steps
Findings claimed without evidence
Severity Accuracy
5
CVSS scores justified with vector strings; severity matches actual impact
Default High/Critical without justification
AI Use Disclosure
3
Complete log of AI interactions with validation
Undisclosed AI use (academic misconduct)
Professional Tone
2
Could be delivered to a real client
Informal language; typos throughout
Chained Attacks
2
Identifies and demonstrates at least one finding chain
No chains identified
4.7 Final Grade Calculation
Total score
Letter
Description
90–100
A
Mastery. Found Hard-tier vulnerabilities + professional report quality.
Per-student instances. Each student runs their own Docker stack locally; no shared instance to exfiltrate flags from.
Score-board authority. The score board is the only authority on flag correctness; flags in the local code are obfuscated or held in env vars / external state the student doesn't trivially grep for.
Flag rotation. Instructor regenerates flags between cohorts via npm run regenerate-flags in instructor/. The script rewrites both instructor/flags_master.json and the embeddings in source.
Submission rate limit. 60 submissions/min per assessment_id prevents brute-force flag guessing.
Code obfuscation. Flags in source are split across files and partly assembled at runtime; no single grep retrieves all of them.
Time-stamped captures. Score board records exact capture time; suspicious patterns (10 hard-tier flags in 60 seconds; identical capture sequences across two students) are flagged for instructor review.
Phase 1.docker-compose up produces a running app at :3000 with login, notes CRUD, orders, admin panel, and an empty score board service at :3001. Schema in §5.3 deployed; seed data loaded.
Phase 2. All 12 trivial+easy vulns exploitable by an instructor walkthrough in under 30 minutes; flags returned by the score board.
Phase 3. All 8 medium vulns exploitable; score board validates and returns scaled scores; rate limiter active.
Phase 4–5. All medium-hard and hard vulns exploitable, including the SSRF→admin chain end-to-end.
Phase 6. Instructor view shows live progress for ≥ 5 simulated students; flag rotation script regenerates a working set.
Phase 7. Pilot students reach a normal-distribution score range (40–85% completion); no false positives surface.
Phase 8.instructor/SOLUTIONS.md walks every flag end-to-end; ASSESSMENT_BRIEFING.md matches Appendix A.
Global
All 32 vulnerabilities are exploitable and have unique flags.
Docker setup completes in under 5 minutes on standard developer hardware (M1/M2 Mac, modern x86 laptop).
Score board correctly validates all 30 flags; rejects malformed; rate-limits at 60/min.
Average student finds all trivial + 5–7 easy in the first hour.
No false-positive vulnerabilities (every finding the score board validates is intentional).
Application supports 30 concurrent students per Docker host.
Solution guide for instructors is complete and tested.
7.3 Risks and Mitigations
Risk
Impact
Mitigation
Students share flags via chat
Invalidates assessment
Per-student instances + flag rotation per cohort + monitor capture timing
Docker setup fails on student laptop
Wasted time
Pre-flight script validating Docker install + version; backup VM image on USB
Hard tier too hard — nobody finds them
Scoring imbalance
Pilot test; adjust difficulty if < 10% of pilot students find a hard vuln
Score board crashes during assessment
Cannot grade
Health monitoring + automatic restart; manual flag verification fallback (instructor's flags_master.json)
Vulnerability fix in dependency makes vuln un-exploitable
Vulns become non-findings
All deps pinned in package.json; CI smoke-tests confirm each vuln still returns its flag
Application is too easy — most score 90+
Compresses grading
Difficulty calibration via pilot; adjust scoring weights or add deeper hard-tier vulns
7.4 Future Enhancements
GraphQL endpoint variant for testing introspection and batch attacks.
Mobile app version (Android) testing client-side and platform-specific issues.
Multi-tenant variant where the student must escape their tenant boundary.
Cloud-deployed version (AWS/Azure) with cloud-specific misconfigurations.
Time-limited mode for shorter assessments (90-minute version with 15 vulns).
Adaptive difficulty: score board reveals hints if student is stuck > 20 min.
Appendix A — Sample Student Briefing
This is the briefing students see at the start of the assessment.
CHALANEE — Final Capstone Assessment
=====================================
Welcome to your final capstone assessment. You have 4.5 hours.
TARGET: Chalanee, a personal digital vault application running locally
on your machine.
RULES OF ENGAGEMENT:
1. You may test ONLY your own local instance.
2. You may use any tool: Burp, ZAP, curl, scripts, AI assistants
(with disclosure).
3. You MUST capture flags via the score board to receive points.
4. You MUST submit a professional penetration test report.
5. Sharing flags with other students = academic misconduct.
6. Modifying Chalanee source code to extract flags = academic
misconduct.
SCORING:
50% flag capture (87 raw points scaled to 50)
50% report quality (50 points)
60+ total to pass; 80+ for distinction; 90+ for mastery.
DELIVERABLES (due in 4.5 hours):
• Score board completion screenshot
• Professional penetration test report (template provided)
• AI use disclosure (mandatory if used)
Begin when ready. Good hunting.
Appendix B — Sample Flag Submission Flow
Student finds reflected XSS at /search?q=<script>alert('XSSTEST')</script>.
Alert appears; XSS confirmed executing.
Student observes the flag in the response body: FLAG{search-reflects-everything}.
Student opens the score board: http://localhost:3001.
Student enters their assessment_id and the captured flag.