Rexigon
A shop that sells collectible toy dinosaurs, grouped into five sets. The whole catalogue lives on one long page, so each set has a share-friendly deep link that jumps you straight to it once the page loads, all handled in your browser.
The Scenario
Rexigon is a small Oregon shop that sells collectible toy dinosaurs, sorted into five sets from the big tyrannosaurs to the marine reptiles. Because the whole range sits on one long catalogue page, a developer added a shortcut so a link can carry which set to jump to and the page scrolls there on its own when it opens. He wired it up with the shop's old front-end library because it was already on the page, and it worked the first time he tried it.
Challenge Intel
Synopsis
DOM-based XSS through the jQuery $() selector sink. /collection.php loads jQuery 1.6.2 and a jump-to-set script that decodes location.hash and passes it into $(). In 1.6.2 the selector regex still treats a leading '#' followed by '<...>' as HTML, so a fragment like #<img src=x onerror=...> is built as a node in the live document and the inline onerror runs.
What It Is
The catalogue page /collection.php loads /static/jquery-1.6.2.min.js and then /static/jump.js. jump.js is a jump-to-set shortcut: on DOM ready and on every hashchange it runs var $el = $(decodeURIComponent(location.hash)) and scrolls to the matched element. The browser percent-encodes the angle brackets in location.hash, so the decode is what restores the literal '<' and '>', and the decoded fragment is handed to $() with the leading '#' still attached. jQuery 1.6.2's quickExpr is /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w-]*)$)/, and the HTML branch [^<]* happily consumes a leading '#', so a hash such as #<img src=x onerror=...> matches the HTML branch and jQuery builds the markup as a real node in the live document instead of reading it as an id selector. The id-guard that stops this (the [^<] tightening to [^#<]) landed between 1.6.2 and 1.6.3, so the pinned 1.6.2 is exploitable while any later 1.x or 3.x is not. The built img has a failing src, so its inline onerror fires. The fragment never leaves the browser, so this is pure DOM XSS with no server round trip. Every page sends Content-Security-Policy-Report-Only with script-src 'self' and no unsafe-inline, so the inline handler trips a script-src-attr violation with blocked-uri inline. The browser posts that report to /__csp-report.php, whose document-uri contains /collection.php, the session is marked solved, and /__status.php returns the flag. Canonical payload: /collection.php#<img src=x onerror=alert(document.domain)>. The correct fix is to validate the fragment as an id (for example with /^[\w-]+$/) before passing it to $(), or read location.hash.slice(1) through document.getElementById, or upgrade jQuery to 3.5 or later, which both adds the id-guard and parses HTML in an inert document so a detached image onerror never runs.
Who It's For
Players who know reflected and stored XSS and want a DOM-only sink where the source is the URL fragment and the sink is an old jQuery $() call that turns a selector string into live HTML.
Skills You'll Practice
- Tracing the URL fragment (location.hash) from source to a jQuery $() sink
- Recognising that pre-3.5 jQuery $() treats a string containing markup as HTML, not a selector
- Reading a library version against the exact point where a sink was hardened
- Building a node in the live document so an inline onerror handler runs
What You'll Gain
- Confidence that $(userInput) in old jQuery is an HTML-injection sink, not just a lookup
- The fragment-to-$() source-and-sink pattern and why it never has to round-trip the server