Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

XSS Practice Lab

A self-hosted, hands-on Cross-Site Scripting practice academy — 72 labs, beginner through pro. All 72 are built and live.

Run it

unzip xss-bugs-lab
cd xss-bugs-lab
npm install
npm run dev

Then open http://localhost:3000 — it redirects straight to the dashboard.

npm run dev starts two things: the main app on port 3000 (unchanged), and a small zero-dependency static server on port 3001 used only by the postMessage labs (PM1-PM3) and a couple of CSP/clobbering labs to provide a genuinely different origin for realistic cross-origin testing — see scripts/dev.js. Nothing else changes; it's still one command, no Docker, no external database, no signup. Progress is stored in a local SQLite file at data/xss-lab.sqlite, created automatically on first run.

What's here

All 9 categories, 72 labs total:

  • Reflected XSS (8) — query params, path params, POST bodies, attribute breakout, error pages, redirect params, header reflection, mixed encoding
  • Stored XSS (8) — comments, profile bio, username, forum replies, support tickets (admin-only view), product reviews, file names, private messages
  • XSS Contexts (8) — HTML body, quoted attribute, unquoted attribute, event handler, inline JS string, URL/href, CSS, JSON-in-<script>
  • DOM XSS (10) — hash, query string, in-memory client state, localStorage, sessionStorage, referrer, live template rendering, unsafe navigation, multi-step transform, insertAdjacentHTML
  • Filtering & Sanitization (10) — case-variation bypass, incomplete tag/attribute blacklists, double-decode bypass, client-only sanitization, create-vs-update inconsistency, allowlist misconfiguration, parser mismatch, single-pass filter recombination, context-mismatched sanitization
  • Modern Framework XSS + postMessage (8) — real live React components, a real live Vue 3 app, a faithfully-reproduced Angular pattern, client-side template injection, three postMessage labs with a genuinely separate attacker origin
  • CSP & Trusted Types (8) — unsafe-inline, unsafe-eval, a whitelisted-domain JSONP bypass, nonce reuse, a missing base-uri base-tag hijack, a weak Trusted Types policy, a misconfigured default policy, a real mutation-XSS (mXSS) sanitizer bug
  • DOM Clobbering & Prototype Pollution (6) — implicit-global clobbering, getElementById clobbering, pollution via URL parameters, pollution chained into an unrelated gadget, pollution via a shared library file, a combined clobbering+pollution chain
  • Black-Box / Real-World (6) — realistic apps with no vulnerability type disclosed; you find it the way you would on a real engagement

How a lab works

  1. Open the lab from the dashboard → its category → the lab itself.
  2. The right panel has the description, objective, hints (up to 3), and a flag box.
  3. The left panel embeds the actual vulnerable target app (also viewable in its own tab).
  4. Find the injection point and get your payload to call window.__labImpact('<LAB_ID>') — every vulnerable page exposes this function. It's not a fixed payload string to memorize; any technique that gets your JavaScript to run and call that function works.
  5. Once it fires, the target page shows a banner with your flag. Copy it into the flag box to complete the lab and earn XP.
  6. "Reset lab" clears your progress and any stored content for that lab so you can retry from scratch.

For the six Black-Box labs specifically, the vulnerability type is intentionally not named anywhere in the lab description — that's the point of that category.

How this was built, and where to double-check my work

I built this in 8 phases (Reflected → Stored → Contexts → DOM → Filtering → Framework/postMessage → CSP/Trusted Types → Clobbering/Pollution → Black-Box), testing as I went. Where I could verify something with curl (every server-rendered lab: all of Reflected, Stored, Contexts, Filtering, most of Framework, CSP header values, and all the injection-point plumbing), I actually did — hand-crafting the exact exploit payload and confirming the real server-side code produces the expected unescaped output, not just eyeballing the code.

Where I couldn't verify with curl — anything that only manifests through real browser JavaScript execution — I've flagged clearly below, roughly in order of how much browser-specific subtlety is involved (least to most):

  • DOM XSS (D1-D10) and the DOM-XSS-flavored Black-Box labs (BB3, and BB6's reflected half) are pure client-side JS (innerHTML/document.write/insertAdjacentHTML assignments). I hand-traced the source-to-sink logic and it's standard, well-established DOM API behavior — I'm confident in it, but it's untested by an actual browser on my end.
  • Prototype Pollution (PP1-3) is pure JavaScript prototype-chain mechanics, no browser-parsing involved. I hand-traced each one's exact property lookup/assignment sequence line by line to confirm Object.prototype genuinely gets polluted app-wide (not just on a local copy) — this is where I'm most confident of anything non-curl-testable in the project. It also caught a real mistake: my first draft of PP3 used a "clone" function that, on close tracing, would not have achieved true global pollution — only a merge-into-existing-target pattern does, which is what's actually implemented.
  • DOM Clobbering (DC1-3) relies on browser-specific named-property-exposure behavior. I verified the exact technique (anchor-collection + name attribute) against current PortSwigger Research documentation rather than from memory, but couldn't confirm live execution myself.
  • CSP & Trusted Types (CSP1-5, TT1-2, MX1) is the most browser-subtle category here. CSP enforcement, Trusted Types enforcement, and MX1's mutation-XSS parsing quirk all require a real browser to observe. I specifically web-searched to verify the CSP5 base-tag bypass and the MX1 noscript/title mXSS technique against real documented sources (including PortSwigger's own lab writeups) rather than relying on memory. This category is worth your own hands-on verification first, more than any other.
  • React/Vue specifics (FW1-FW5): FW1/FW2 are real React components (this whole app is Next.js/React) and FW3 is a real, live Vue 3 app using Vue's own official "global build," vendored locally with no CDN dependency. FW4 (Angular) is not a live Angular app — Angular's modern tooling needs a real bundler, which didn't fit this project's no-build-step approach for one lab, so it's a faithful vanilla-JS reproduction of the exact bypassSecurityTrustHtml vulnerability pattern instead, clearly labeled as such in the lab. Worth calling out: while building FW2 I initially assumed React doesn't validate URL schemes on href — testing it directly showed that's wrong for the React version here (19.x blocks javascript: hrefs by default now). Caught by testing, not by trusting the assumption; rebuilt FW2 around a different, still-real React gotcha (a ref-based DOM write that bypasses JSX escaping).

A few infrastructure decisions worth knowing about if you extend this:

  • Uses Node's built-in node:sqlite instead of better-sqlite3 — no native compilation step, so npm install stays simple everywhere, including offline/locked-down machines. node:sqlite returns rows as null-prototype objects, which React Server Components refuse to pass to Client Components — src/lib/db.ts spreads every row into a plain object (toPlain()) before it can reach a component. This one actually broke the dashboard once real progress data existed, in an earlier build pass — caught it, fixed it at the source, and re-verified every page with a full dataset afterward.
  • Added PRAGMA busy_timeout after a genuine stress test surfaced real "database is locked" errors under concurrent requests (the lab page polls its own progress every 2s, which can overlap with hint/flag/reset actions) — SQLite now retries for up to 5s instead of failing immediately.
  • PM1-PM3 (and CSP3/CSP5/DC1/DC2/DC3) use a real second origin (http://localhost:3001) rather than simulating cross-origin behavior same-origin, specifically so origin-check bypasses are genuinely demonstrable, not just theoretical.
  • Uses system font stacks rather than next/font/google, since Google Fonts requires a network fetch at build time that would break "works offline after npm install."
  • Single local learner account, no login system — progress tracked via an auto-created session cookie.

About

Cross Site Script all type of bugs lab practice here in local env

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors