github.com/hahwul/xssmaze
0.3.0 / published Aug 2, 2026 / repository
XSSMaze is a web service designed to test and improve the performance of security testing tools by providing various cases of XSS vulnerabilities.
XSSMaze is an intentionally vulnerable web application for measuring and improving XSS detection in security testing tools. It serves 1000+ endpoints across 170+ categories — reflected, DOM, stored, and header/path/body injection, plus filter and WAF bypasses, CSP gadgets, prototype pollution, template injection, and modern DOM sink/source shapes that defeat naive taint analysis.
Every endpoint ships structured metadata, so a benchmark can score per vulnerability class instead of regex-guessing at the served HTML.

[!WARNING] This app is deliberately vulnerable. It binds to
127.0.0.1by default — only pass-b 0.0.0.0on a network you trust.
Install
docker run -p 3000:3000 ghcr.io/hahwul/xssmaze:main
Or from source:
shards install && shards build
./bin/xssmaze
Usage
./bin/xssmaze [options]
-b, --bind HOST address to bind 127.0.0.1
-p, --port PORT port to listen on 3000
-s, --ssl serve over HTTPS
--ssl-key-file FILE private key, PEM encoded
--ssl-cert-file FILE certificate, PEM encoded
-q, --quiet do not log requests
--no-banner start without the banner
--no-color disable ANSI colour
-v, --version print the version and exit
-h, --help print this help and exit
Colour is dropped automatically when the output is not a terminal, and
NO_COLOR is honoured.
Endpoint map
| Endpoint | Returns |
|----------|---------|
| /map/text | newline-separated URLs |
| /map/json | full metadata; filter with ?type=, ?q=, ?vuln=, ?reach=, ?exploitable= |
| /map/markdown | markdown table |
| /map/categories | categories with counts + class/reach rollups |
| /map/openapi | OpenAPI 3.0 catalog |
| /sitemap.xml | sitemap of all maze paths |
| /stats | aggregate counts by class, reach, source, and sink |
| /health | liveness probe (/healthz alias) |
| /version | version + counts |
| /random | 302 to a random maze |
curl "http://localhost:3000/map/json?vuln=dom" # only DOM flows
curl "http://localhost:3000/map/json?reach=server" # payload fits in an HTTP request
curl "http://localhost:3000/map/json?exploitable=false" # deliberate true negatives
The index page (/) has a client-side filter and links to every map above. Map responses
are built once at startup, cached, and gzip pre-compressed (Accept-Encoding: gzip cuts
the index by ~85%), so they are safe to poll from tooling.
Vulnerability metadata
Every endpoint in /map/json carries a vuln object:
{
"name": "dom-level7",
"url": "/dom/level7/",
"params": ["#hash"],
"vuln": {
"class": "dom",
"reach": "client",
"delivery": ["fragment"],
"sources": ["location.hash"],
"sinks": ["innerHTML"],
"exploitable": true,
"note": null
}
}
| Field | Meaning |
|-------|---------|
| class | reflected-html, reflected-attr, reflected-js, dom, stored, prototype-pollution, csti, non-xss-control, or unclassified |
| sources / sinks | DOM taint endpoints, e.g. location.hash → innerHTML |
| delivery | where the payload enters: query, path, body, header, cookie, referer, fragment, postmessage, window-name, … |
| reach | derived — server if any delivery channel fits an HTTP request, client if the payload only exists browser-side, unknown if untriaged |
| exploitable | false marks a deliberate control / true negative |
| note | caveats: required interaction, why it is a control, non-obvious parameter names |
Classification follows the injection context — where the bytes land — not what the
receiving API does with a well-formed argument. A value reflected raw into a JS string
literal is reflected-js however inert the function it is passed to.
Two things this exists to stop a benchmark getting wrong:
reach: "client"endpoints (fragment, postMessage, window.name, clipboard, drag-and-drop) cannot be reached by a request-only scanner at all. Counting them as misses measures the wrong thing.exploitable: falseendpoints are not bugs. The wholexsleakcategory is cross-site leaks, not XSS — a scanner that reports nothing there is correct.
Untriaged endpoints are "unclassified" with reach: "unknown", deliberately distinct
from "reviewed and found safe".
Security header overrides
To calibrate scanners against different defensive configurations, any endpoint accepts per-request header overrides via query params:
| Param | Sets |
|-------|------|
| set_csp | Content-Security-Policy (URL-encode spaces/quotes) |
| set_xcto | X-Content-Type-Options (e.g. nosniff) |
| set_xfo | X-Frame-Options (e.g. DENY) |
curl -i "http://localhost:3000/basic/level1/?query=a&set_csp=default-src%20%27self%27"
XS-Leaks
xsleak-* levels are cross-origin side-channels that vary response size, subresource
count, load/error behavior, timing, and redirect depth by a "secret" state. State comes
from either q=admin or the xsleak_role=admin cookie (set via GET /xsleak/login?as=admin).
| Level | Endpoint | Oracle |
|-------|----------|--------|
| 1 | /xsleak/search?q=admin | body size (admin returns more results) |
| 2 | /xsleak/frame?q=admin | frame count |
| 3 | /xsleak/avatar.gif?q=admin | load/error (admin 200, guest 404) |
| 4 | /xsleak/timing?q=admin | timing (guest path sleeps longer) |
| 5 | /xsleak/redirect?q=admin | redirect-chain depth |
These are leaks, not XSS — they are marked exploitable: false and a scanner reporting
nothing here is behaving correctly. Spot the difference from the CLI:
curl -s "http://localhost:3000/xsleak/frame?q=guest" | wc -c
curl -s "http://localhost:3000/xsleak/frame?q=admin" | wc -c
curl -sL -o /dev/null -w "%{time_total}\n" "http://localhost:3000/xsleak/timing?q=admin"
To measure them properly, host a page on a different origin and probe with load/error
handlers, timing, and iframe.contentWindow.length.
Benchmarking scanners
scripts/benchmark.py pulls every endpoint from /map/json, runs a scanner against
them, and prints a detection scorecard.
./bin/xssmaze -b 0.0.0.0 # terminal 1
cd scripts && ./benchmark.sh http://localhost:3000 # terminal 2
Nuclei is supported out of the box; any other tool can be wired in with
--custom-scanner "mytool {URL}". See scripts/README.md for
options, report formats, and how to add a scanner permanently.
License
MIT
API
- EventHandlerHelper
- Filters
- Maze
- WafFacade
Shared chrome + helpers for the branded block pages.
- Xssmaze
Request logging, in place of Kemal's
Log-based handler.