June 21, 2026
Hijacking a Link: My Eighth XSS Lab on PortSwigger
I’ve been working through the PortSwigger Web Security Academy XSS labs one by one, and this eighth lab combined two ideas I’d already seen…
Diya
4 min read
I've been working through the PortSwigger Web Security Academy XSS labs one by one, and this eighth lab combined two ideas I'd already seen separately. It's a stored XSS, like the comment-based lab earlier in this series, but the injection point is an anchor href attribute, similar to the jQuery href lab. Here's how it went.
Category: Stored Cross-Site Scripting (XSS)
Difficulty: Apprentice
Lab Link: Stored XSS into anchor href attribute with double quotes HTML-encoded
Objective
Submit a comment that calls the alert function when the comment author's name is clicked.
Vulnerability Overview
This lab demonstrates a stored XSS vulnerability in the blog's comment functionality. The comment form includes a "Website" field, and whatever value is submitted there is stored on the server and later rendered as the href attribute of an anchor tag wrapping the commenter's name.
Double quotes are HTML-encoded in this field, so a payload like"onclick="alert(1) would not work, since the closing quote needed to break out of the href attribute gets neutralized. However, the application does not validate or restrict the protocol used in the URL itself. This opens the door to the javascript: pseudo-protocol, which executes arbitrary JavaScript when the link is clicked, without needing to break out of the attribute at all.
Steps to Reproduce
- Opening the lab reveals the blog home page with a list of posts. Selecting one ("The Hating Dating App") leads to the full post with a comment section at the bottom.
- Scrolling to the bottom of the post reveals the "Leave a comment" form, with fields for Comment, Name, Email, and Website.
- Rather than entering a real URL, the Website field is filled with:
javascript:alert(1)javascript:alert(1)The rest of the form is filled normally:
- Comment:
Hello, World! - Name:
diya - Email:
test@test.com - Website:
javascript:alert(1)
Clicking "Post Comment" submits the form and confirms the submission.
- Returning to the blog post and scrolling to the comments section shows the new comment, with the name "diya" now rendered as a clickable link, unlike the other commenters whose names are plain text.
Inspecting the element via DevTools confirms the payload was stored and reflected without any sanitization:
<a id="author" href="javascript:alert(1)">diya</a><a id="author" href="javascript:alert(1)">diya</a>
- Clicking the "diya" link executes the
javascript:URL, firing the alert.
The lab is marked as solved as soon as the alert fires. Interestingly, in this particular run the lab status flipped to "Solved" almost immediately after posting the comment, even before manually clicking the link. This is likely because PortSwigger's lab verification also simulates the click in the background to confirm the payload works, in addition to it being trivially reproducible by clicking the link manually as shown above.
Technical Evidence
The key evidence is the rendered anchor tag captured in Step 4:
<a id="author" href="javascript:alert(1)">diya</a><a id="author" href="javascript:alert(1)">diya</a>The application HTML-encodes double quotes in the Website field, which would normally prevent an attacker from closing the href attribute early and injecting a separate event handler attribute (e.g., "onclick="alert(1)). However, encoding double quotes does nothing to restrict the content of the attribute itself. Since the entire Website value is placed inside href="..." without any protocol validation, submitting javascript:alert(1) results in a syntactically valid anchor tag that executes JavaScript the moment it's clicked, no attribute breakout required.
Root Cause
The application stores and reflects user-supplied data into the href attribute of an anchor tag without validating the URL scheme. While double-quote encoding protects against attribute-breakout attacks, it does not protect against javascript: URLs, which are valid href values from the browser's perspective and execute as code when the link is activated. This is a common oversight: encoding characters is necessary but not sufficient when the attribute itself accepts executable content, like a URL.
Impact
Because this is a stored XSS, the payload persists on the server and executes for every visitor who clicks the malicious commenter's name, not just the original attacker. Potential consequences include:
- Mass session hijacking, affecting any user (including administrators) who clicks the link while viewing the page
- Performing actions on behalf of the victim within the application
- Redirecting victims to phishing or malware-distributing sites
- Using the stored payload as a foothold for further attacks against anyone who interacts with the page
Unlike some stored XSS scenarios that fire automatically on page load, this one requires a click, but a comment author's name is a natural, low-suspicion thing for a visitor to click.
Remediation
To prevent this type of vulnerability, the application should:
- Validate the URL scheme of any user-supplied value placed into an
hrefattribute, allowing only safe schemes such ashttp:,https:, andmailto:, and rejecting anything else, includingjavascript:,data:, andvbscript:. - Use an allowlist-based URL sanitizer rather than relying solely on character encoding, since encoding alone does not address scheme-based attacks.
- HTML-encode all special characters in the attribute value as a baseline defense, including double quotes, to also prevent attribute-breakout style attacks.
- Implement a Content Security Policy (CSP) to restrict script execution as a defense-in-depth measure, which can mitigate
javascript:URL exploitation even if input validation is missed somewhere.
Conclusion
This lab was a useful reminder that output encoding and input validation solve different problems. Encoding the double quotes closed off one attack path, breaking out of the attribute, but left another wide open, since the href attribute itself can hold executable content if the scheme isn't checked. Combined with the stored nature of the vulnerability, this is a good example of how a single overlooked validation step can turn a single comment field into a vector that compromises every visitor who clicks a name. Eight labs in, and it's becoming clear that thorough XSS prevention means checking not just what characters are allowed, but what those characters, taken together, are capable of doing in context.