June 18, 2026
From Reflected to Stored: My Second XSS Lab on PortSwigger
After solving the reflected XSS lab, I moved on to the next challenge in the PortSwigger Web Security Academy: a stored cross-site…
Mahdiyaa
4 min read
After solving the reflected XSS lab, I moved on to the next challenge in the PortSwigger Web Security Academy: a stored cross-site scripting vulnerability hidden in a blog's comment functionality. This one turned out to be even more interesting, since the payload doesn't just disappear after one request — it sticks around and fires every time the page is loaded.
Category: Cross-Site Scripting (XSS)
Difficulty: Apprentice
Lab Link: PortSwigger Web Security Academy
Objective
The goal of this lab is to submit a blog comment that calls the alert() JavaScript function when the blog post is viewed, demonstrating a stored XSS vulnerability where user input is saved on the server and later rendered into the page without any encoding.
Vulnerability Overview
This lab simulates a blog application that allows visitors to leave comments on posts. Each comment is submitted through a form and stored on the server. When the post page is loaded, all of its comments including the one just submitted are rendered directly into the HTML response. Because the application performs no output encoding when displaying these stored comments, any HTML or JavaScript submitted as part of a comment is rendered and executed by the browser of every single visitor who views that post afterward.
Steps to Reproduce
- Navigate to the lab and open one of the blog posts.
- Scroll down to the "Leave a comment" section and fill in the comment form with the following payload, along with a name and email:
<script>alert(1)</script><script>alert(1)</script>- Click Post Comment to submit the form. The application responds with a confirmation page:
Thank you for your comment!
Your comment has been submitted.Thank you for your comment!
Your comment has been submitted.At this point, the comment is already stored on the server, but the script has not executed yet, since this confirmation page doesn't render the comment itself.
- Click "Back to blog" to return to the post. As the page loads, it renders the full list of comments — including the malicious one. Since the comment is reflected straight into the HTML without encoding, the injected
<script>tag executes immediately, triggering a JavaScriptalert()popup.
Technical Evidence
Inspecting the raw HTTP response of the post?postId=4 request via the browser's DevTools (Network tab → Response) confirms that the payload is stored and rendered verbatim, with no HTML entity encoding applied:
<section class="comment">
<p>
<img src="/resources/images/avatarDefault.svg" class="avatar">
</p>
<p><script>alert(1)</script></p>
<p></p>
</section><section class="comment">
<p>
<img src="/resources/images/avatarDefault.svg" class="avatar">
</p>
<p><script>alert(1)</script></p>
<p></p>
</section>
Compared to a legitimate comment rendered just above it in the same response, which is displayed as plain text:
<section class="comment">
<p>
<img src="/resources/images/avatarDefault.svg" class="avatar">
</p>
<p>I asked my wife which one of your blogs she thought was best...</p>
<p></p>
</section><section class="comment">
<p>
<img src="/resources/images/avatarDefault.svg" class="avatar">
</p>
<p>I asked my wife which one of your blogs she thought was best...</p>
<p></p>
</section>This confirms that the application stores comment content exactly as submitted and inserts it directly into the page template without sanitizing or encoding it, allowing arbitrary script execution for every visitor who views the post.
Result
The lab is successfully solved as soon as the stored payload executes and the alert(1) popup fires while viewing the post.
Root Cause
The application accepts free-form text through the comment form and stores it as-is in its backend. When rendering the post page, it concatenates the stored comment content directly into the HTML response without applying any output encoding (e.g., converting < and > into their corresponding HTML entities). This allows an attacker to inject arbitrary HTML/JavaScript that gets persisted and executed in the browser of anyone who later visits the page.
Impact
Stored XSS is generally considered more dangerous than reflected XSS, because the attacker doesn't need to trick a victim into clicking a crafted link — the malicious script is already embedded in the page and executes automatically for every visitor. Potential consequences include:
- Mass session hijacking, affecting every user (or even administrators) who views the infected page
- Persistent defacement of the page for all visitors, not just one targeted victim
- Theft of sensitive information entered by users who interact with the compromised page
- Use of the stored payload as a foothold to deliver further attacks, such as redirecting visitors to phishing or malware sites
Remediation
To prevent this type of vulnerability, the application should:
- Encode output based on context — HTML-encode any stored user content before rendering it into HTML body content (e.g., < →
<, > →>). - Sanitize input on submission and/or output, especially in fields where rich formatting isn't required, by stripping or escaping HTML tags.
- Use a well-tested sanitization library (e.g., DOMPurify) if rich text input genuinely needs to be supported, rather than allowing raw HTML to be stored and rendered.
- Implement a Content Security Policy (CSP) as a defense-in-depth measure to restrict the execution of inline scripts even if a payload slips through.
- Use templating engines with automatic context-aware escaping instead of manually concatenating stored strings into HTML.
Conclusion
This lab was a great follow-up to the reflected XSS challenge, since it highlighted a key difference between the two vulnerability types: a stored payload doesn't need a victim to click anything — it just sits there, waiting to fire on anyone who visits the page. It's a strong reminder that any feature accepting free-form user input, like a comment box, needs proper output encoding before that input is ever rendered back into HTML. Excited to keep working through more labs and explore other XSS variants next.