Authorization disclaimer. This walkthrough is limited to the intentionally vulnerable PortSwigger Web Security Academy lab linked below. Test only systems you own or are explicitly authorized to assess.

Overview

This lab isolates DOM selector injection on hashchange. A hashchange listener decodes location.hash and concatenates it into section.blog-list h2:contains(...), which is passed to jQuery. Loading the iframe and then changing its fragment fires hashchange; old jQuery treats a selector beginning with markup as HTML.

Lab objective/setup

Open the Academy exercise at the reserved lab URL and work only in that disposable environment. The required proof is specific: Delivery to the simulated victim invokes print() and solves the lab.

What I observed

A hashchange listener decodes location.hash and concatenates it into section.blog-list h2:contains(...), which is passed to jQuery. Loading the iframe and then changing its fragment fires hashchange; old jQuery treats a selector beginning with markup as HTML.

Academy objective identifies `location.hash` as the source and jQuery `$()` as the selector sink, requiring an exploit delivered to a victim.
Academy objective identifies `location.hash` as the source and jQuery `$()` as the selector sink, requiring an exploit delivered to a victim.
Client script registers `hashchange`, decodes the fragment, concatenates it into an `h2:contains(...)` selector, and scrolls to the matched post.
Client script registers `hashchange`, decodes the fragment, concatenates it into an `h2:contains(...)` selector, and scrolls to the matched post.
Page source shows the first blog-card heading and anchor structure consumed by the selector.
Page source shows the first blog-card heading and anchor structure consumed by the selector.
Page source shows additional `h2` headings in the same blog list, confirming the selector operates over attacker-influenced text.
Page source shows additional `h2` headings in the same blog list, confirming the selector operates over attacker-influenced text.
Address bar contains benign fragment `#Perseverance` for a normal heading match.
Address bar contains benign fragment `#Perseverance` for a normal heading match.

Why it is vulnerable

The application applies protection for the wrong parser or omits a control at the decisive boundary. In this case, a hashchange listener decodes location.hash and concatenates it into section.blog-list h2:contains(...), which is passed to jQuery. Browser parsing therefore preserves the attacker-controlled behavior instead of reducing the value to inert text or a safe URL.

Walkthrough in the authorized PortSwigger lab

First reproduce the recorded page or request and inspect the exact reflected, stored, or DOM-generated context described above. Then supply the following lab payload as data, not as active markup in this article:

1
`<iframe src="LAB/#" onload="this.src+='&lt;img src=x onerror=print()&gt;'"></iframe>`

Loading the iframe and then changing its fragment fires hashchange; old jQuery treats a selector beginning with markup as HTML.

Browser scrolls to the Perseverance article card after the benign fragment triggers the hash-change selector.
Browser scrolls to the Perseverance article card after the benign fragment triggers the hash-change selector.
Address bar contains a fragment with spaces and a name to test URL fragment decoding.
Address bar contains a fragment with spaces and a name to test URL fragment decoding.
Console compares raw `window.location.hash` with `decodeURIComponent`, showing the decoded selector text passed to jQuery.
Console compares raw `window.location.hash` with `decodeURIComponent`, showing the decoded selector text passed to jQuery.
Exploit Server body contains an iframe aimed at the lab and an encoded image-error fragment; script appends the fragment after load to force `hashchange`.
Exploit Server body contains an iframe aimed at the lab and an encoded image-error fragment; script appends the fragment after load to force `hashchange`.
Solved Academy banner confirms the victim iframe processed the malicious fragment and the old jQuery selector treated it as HTML.
Solved Academy banner confirms the victim iframe processed the malicious fragment and the old jQuery selector treated it as HTML.

Verification

Delivery to the simulated victim invokes print() and solves the lab. This distinguishes execution from a payload merely appearing in source or being stored without reaching the required event or sink.

Impact and defensive lesson

In a real application, equivalent XSS could act with the victim’s same-origin privileges, expose readable data, or perform authenticated actions. Fix the data flow at its context: use text-only DOM APIs, JavaScript- or attribute-aware encoding, strict URL-scheme allowlists, and maintained sanitization where HTML is unavoidable. HttpOnly cookies and a restrictive CSP reduce consequences but do not repair the injection flaw.

References