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 Stored DOM XSS through one-shot replacement. Comments are stored, then client code calls comment.replace("<", "&lt;").replace(">", "&gt;") before assigning the result to innerHTML. String-pattern replace changes only the first bracket pair; the sacrificial <> consumes both replacements and leaves the image handler intact.

Lab objective/setup

Open the Academy exercise at the reserved lab URL and work only in that disposable environment. The required proof is specific: Reloading the post renders the stored image, fires onerror, and solves the lab.

What I observed

Comments are stored, then client code calls comment.replace("<", "&lt;").replace(">", "&gt;") before assigning the result to innerHTML. String-pattern replace changes only the first bracket pair; the sacrificial <> consumes both replacements and leaves the image handler intact.

Academy objective identifying stored DOM XSS in blog comments and requiring an alert call.
Academy objective identifying stored DOM XSS in blog comments and requiring an alert call.
Client-side `escapeHTML` uses two non-global `replace` calls, encoding only the first less-than and greater-than characters before the result reaches `innerHTML`.
Client-side `escapeHTML` uses two non-global `replace` calls, encoding only the first less-than and greater-than characters before the result reaches `innerHTML`.

Why it is vulnerable

The application applies protection for the wrong parser or omits a control at the decisive boundary. In this case, comments are stored, then client code calls comment.replace("<", "&lt;").replace(">", "&gt;") before assigning the result to innerHTML. 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
`&lt;&gt;&lt;img src=x onerror=alert(1)&gt;`

String-pattern replace changes only the first bracket pair; the sacrificial <> consumes both replacements and leaves the image handler intact.

Browser alert dialog showing `1` after the sacrificial first bracket pair leaves the later image error handler executable.
Browser alert dialog showing `1` after the sacrificial first bracket pair leaves the later image error handler executable.
Rendered comment contains two broken-image icons and visible inert `<>` text, confirming how the one-shot replacement altered the stored value.
Rendered comment contains two broken-image icons and visible inert `<>` text, confirming how the one-shot replacement altered the stored value.

Verification

Reloading the post renders the stored image, fires onerror, 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