xssmaze
Version, currently 0.3.03 versions
- 0.3.0latestAug 2, 2026
- 0.2.0not indexedAug 2, 2026
- 0.1.0not indexedAug 2, 2026
github.com/hahwul/xssmaze
XSSMaze is a web service designed to test and improve the performance of security testing tools by providing various cases of XSS vulnerabilities.
37 stars
0 dependents
License: MIT
Installation
# Add this to your shard.yml
dependencies:
xssmaze:
github: hahwul/xssmaze
version: ~> 0.3.0Then run:
shards installshard.yml
- Crystal
1.8.2- License
- MIT
- Author
- hahwul <hahwul@gmail.com>
- Target
xssmazefrom src/xssmaze.cr
Dependencies
Runtime Dependencies
- kemal*github: kemalcr/kemal
Development Dependencies
- ameba*github: crystal-ameba/ameba, branch: masterdev
- spec-kemal*github: kemalcr/spec-kemaldev
README
<img src="images/logo.png" alt="XSSMaze" width="260">
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
```bash
docker run -p 3000:3000 ghcr.io/hahwul/xssmaze:main
```
Or from source:
```bash
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 |
```bash
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:
```json
{
"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: 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`) |
```bash
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:
```bash
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.
```bash
./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](scripts/README.md) for
options, report formats, and how to add a scanner permanently.
## License
MIT
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.3.0- Tagged
- Aug 2, 2026
- Commit
2ab9fc913134- Crystal
1.8.2- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/hahwul/xssmaze
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 12, 2026
- Synced
- Aug 12, 2026
- Versions
- 3