package

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

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.1 by default — only pass -b 0.0.0.0 on 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.hashinnerHTML | | 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: false endpoints are not bugs. The whole xsleak category 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