Intentionally vulnerable static website used to test XSS scanners and manual testing techniques against DOM-based cross-site scripting patterns.
⚠️ This website is deliberately insecure. Every vulnerability here is intentional and exists for security testing and learning purposes only. Do not reuse this code in production.
Server-side reflected XSS test targets (like testphp.vulnweb.com) are plentiful, but a static website's DOM XSS is a different attack surface: there is no backend to reflect input, so the vulnerability lives entirely in client-side JavaScript reading location.search or location.hash and writing it into the DOM unsafely.
This repository is a small, self-contained webpage with a few classic vulnerable patterns side by side with a fixed (or safe) equivalent, for comparison.
| # | Sink | Source | Notes |
|---|---|---|---|
| 1 | innerHTML |
?q= query param |
Classic query-string DOM XSS |
| 2 | innerHTML |
# URL fragment |
Never sent to the server — purely client-side, invisible to server logs/WAFs |
| 3 | document.write |
?name= query param |
Different sink behavior than innerHTML (can inject raw <script> tags) |
| 4 | textContent |
?safe= query param |
Safe — same input, properly escaped, shown for comparison |
Visit the deployed page and append a payload like:
https://xss-lab.pages.dev/?q=<img src=x onerror=alert(1)>
https://xss-lab.pages.dev/?name=<script>alert(2)</script>
https://xss-lab.pages.dev/#<img src=x onerror=alert(3)>Or scan with Dalfox
dalfox scan "https://xss-lab.pages.dev/?q=<img src=x onerror=alert(1)>" --skip-discoveryThis website is a useful illustration of how Dalfox v3 actually classifies
DOM-based findings. The sinks here are written entirely by client-side
JavaScript — never by the server — so findings surface as [A]
(source→sink flows identified through static analysis of JavaScript),
rather than [V] or [R].
A precise distinction, confirmed directly by Dalfox's maintainer: [A]
still requires fetching a real HTTP response, since Dalfox analyzes the
JavaScript contained in that response (and external bundles too, if
--analyze-external-js is used). The actual difference between [A] and
most [V]/[R] findings is not "was there a request," but whether the
payload itself needs to appear in the response. Request-based [V]/[R]
findings generally involve the payload showing up there in some form;
[A] findings don't need to, since they're identified by tracing the
code's logic rather than matching the payload's literal presence. (One
exception: out-of-band [V] findings — blind XSS confirmed via a
callback — don't require the payload in the response either, since the
callback itself is the evidence. That mechanism doesn't apply to this lab,
which has no out-of-band sinks, but it's worth knowing "the payload must
appear in the response" isn't a universal rule across every [V] case.)
[V] findings themselves are supported by several kinds of evidence —
including marker selectors, executable schemes landing in dangerous
attributes, injected elements with attached sink handlers, AST-scoped sink
calls found by inspecting the response's own script content, and
inline-handler breakouts — plus the out-of-band callback mechanism noted
above. None of this involves a browser: Dalfox v3 has no headless browser
and no CDP client, by deliberate design — dropping that dependency was one
of the core changes from v2 to v3, and the project's maintainer has stated
there are no plans to reintroduce it.
Because every vulnerability on this page is written into the DOM only
after the browser has already received and rendered a static, unchanging
response, the payload never appears in the actual HTTP response bytes.
That's why this site's findings surface as [A] rather than [V]/[R],
and why --only-poc v correctly returns nothing here — not because the
scan failed, but because none of the evidence types above have anything to
match on a page whose vulnerability exists entirely after the point where
Dalfox reads the response.
Scripting note: Dalfox now attaches a detection_method field to each
finding (e.g. ast, reflection, dom-verification, oob) alongside the
existing [V]/[A]/[R]/[I] tier. The tier lettering is expected to
change as Dalfox's finding model evolves further, but detection_method == "ast" is intended to remain a stable selector for this category of
finding through that transition — worth using instead of the tier letter
if you're scripting against this lab's output long-term.
This lab was used as a real test case in
hahwul/dalfox#1238, which
started as a reported inconsistency (--skip-mining-dom appearing to have
no effect on this site's results) and became a detailed clarification of
Dalfox's detection architecture, directly from its maintainer:
- Dalfox has three independent subsystems: parameter mining (harvesting
candidate parameter names from HTML attributes), AST source→sink analysis
(tracing data flow from sources like
location.search/location.hashto sinks likeinnerHTML/document.writewithin a fetched response's JavaScript), and reflection/DOM-based classification (evaluating whether and how a payload appears in that response, or — for blind XSS — whether an out-of-band callback confirms it).--skip-mining-domonly ever controlled the first of these — it was never the mechanism producing this lab's findings, which is why the flag appeared to have no effect in the original report. [V]does not mean a browser executed the payload — Dalfox has no browser and no CDP client. It means the available evidence (several categories of it, from marker matching to AST inspection of the response's own scripts to out-of-band callbacks) was judged sufficient to classify the finding as vulnerable.- The distinction that actually matters, per Dalfox's maintainer, is not
"static vs. dynamic" — it's how much confidence the available evidence
supports. This lab's findings land at
[A]because their vulnerability is written into the DOM only by client-side JavaScript, after the page's static response has already been fully received. - The issue directly led to
hahwul/dalfox#1244, which
shipped a fix for a misleading summary line (
XSS found 0 XSSpreviously printed above real[A]findings with no indication they existed), surfaced the AST-derived source→sink description in plain terminal output instead of hiding it in JSON only, fixed AST findings reporting a placeholder-instead of the real parameter/source name, and added a permanent "Detection Model" documentation page defining what[V],[A],[R], and[I]mean. - The discussion also led to
hahwul/dalfox#1246, merged,
which added explicit
detection_methodandconfidencefields to every finding, separating "how was this found" from "how confident is Dalfox that it's real." As of this merge, thetypefield (and the[A]tier itself) is unchanged — the new fields currently preview where each finding is expected to land once a future migration completes, but nothing about this lab's[A]classification has changed yet.
An earlier open proposal in the same thread
(hahwul/dalfox#1245)
suggested an opt-in headless verification pass. Dalfox's maintainer has
since clarified there are no plans to build it: dropping a headless
browser dependency was a deliberate, core design decision made moving from
v2 to v3, not a gap awaiting a future fix. If you're evaluating any DOM-XSS
finding — from this lab or elsewhere — that isn't backed by a [V]/[R]
classification, treat it as something to confirm manually in a real
browser, since that confirmation step is expected to remain manual by design, not a temporary limitation.
This is a static website with no build step. Deployed via Cloudflare Pages directly from this repository.
MIT License, please read LICENSE