Mastering XSS Categories: Precise Detection and PoC Crafting in Modern JS Apps

Research Summary

From June–September 2025, public advisories and vendor bulletins confirm a steady cadence of DOM-based XSS across real products, including Adobe Experience Manager and multiple WordPress plugins, with several new CVEs published in August–September 2025 that explicitly cite DOM-XSS as the vector.[^1] [^2] [^3] [^4] PortSwigger’s documentation continues to emphasize hybrid paths where server reflections are consumed by unsafe client sinks (reflected DOM-XSS) and where stored data is later processed by client code (stored DOM-XSS).[^5][^6] MDN’s August 2025 updates reinforce sink-specific behavior (for example, innerHTML, insertAdjacentHTML, document.write) and highlight platform defenses like Trusted Types for HTML/JS/URL sinks.[^7][^8][^9] Feroot’s June 2025 education notes the SPA-centric reality: fragment parameters (#…) and client storage routinely bypass server-side controls, requiring direct DOM source-sink tracing.[^10] Collectively, these verified updates support using fine-grained XSS categories to expand test coverage and produce stronger, reproducible PoCs.

Introduction

Cross-Site Scripting never left. It evolved. In 2025, the highest leverage wins on pentests often come from DOM-centric flaws that server-side scanners never see. Recent disclosures illustrate the point. In July–August 2025, Adobe and NVD published advisories for DOM-based XSS allowing code execution in AEM and related components.[^1][^2][^4] In September 2025, additional DOM-XSS CVEs landed affecting popular plugins where attacker-controlled values flow from URL or stored data to sinks in client code.[^3] Meanwhile, training content and docs updated through mid-2025 continue to stress that modern single-page applications read attacker input from location, message channels (postMessage), API responses, and client storage, then write it into risky sinks such as innerHTML, insertAdjacentHTML, or JavaScript execution contexts.[^5][^6][^7][^8][^10]

This article references the standard categories, Reflected, Stored, DOM-based, and drills into the DOM sub-categories emphasized here: Reflected DOM-XSS and Stored DOM-XSS. The aim is to show exactly where payloads originate, where they land, and how to build PoCs that survive modern browser behaviors (for example, scripts ignored in innerHTML, or event handler restrictions). We will also fold in concrete steps with Burp’s DOM Invader and targeted browser developer tools (devtools) workflows to increase signal and reduce false positives.

Technical Deep-Dive

Categories and why they matter for PoCs

Understanding the categories is critical to discovering XSS flaws, as the location of the vulnerability, delivery path, and execution context determine both detection strategy and PoC mechanics. In all cases the attacker’s objective is to control JavaScript execution in a victim’s DOM, but the route can differ significantly.

  • Reflected XSS (server-side flaw, non-persistent). The server places user input straight into the HTML response. Discovery is straightforward when the reflection is obvious (search pages, error echoes), but output contexts still matter.

  • Stored XSS (server-side flaw, persistent). The server stores the payload and returns it later. Discovery sometimes requires multi-step workflows and role changes (attacker writes → victim browses). Out-of-band paths (email, logs) that later render into HTML are important to consider.

  • DOM-based XSS (client-side flaw, non-persistent in the pure form). JavaScript reads attacker data from a browser-controlled source and writes it to an unsafe injection sink. Common sources used in applications are location.search, location.hash, document.cookie, document.referrer, postMessage data, and JSON from APIs. Common unsafe HTML sinks include innerHTML, outerHTML, insertAdjacentHTML, document.write. When looking inside of JavaScript, execution sinks can include eval, setTimeout(string), Function(), script.text, and URL-based execution such as javascript: in attributes or script.src when mishandled.[^5][^6][^7][^8]

  • Reflected DOM-XSS (hybrid). The request payload is reflected by the server into page markup or a script block; client code later consumes that value and inserts it into a dangerous sink. The flaw is fundamentally in client code.

  • Stored DOM-XSS (hybrid/persistent or limited persistence). The payload is retrieved by client code, perhaps from server storage (via HTML or API) or even from client storage (localStorage, sessionStorage, cookies), and then written to a dangerous sink.

These labels directly impact PoC design. For example, a PoC targeting innerHTML must avoid assumptions that <script> executes; current guidance shows it will not execute from innerHTML across modern browsers, and svg onload may not fire either.[^5] Instead favor attributes such as <img src=x onerror=…> or use iframe side effects and event handlers. Conversely, if you find document.write, a simple <script>alert(document.domain)</script> should still land.[^5]

Source–sink tracing: a rigorous workflow

A repeatable DOM-XSS workflow marries this taxonomy with modern tooling:

Step A: Enumerate sources. In the browser devtools, search the codebase for location, document.cookie, document.referrer, window.name, postMessage, and app-specific state readers. In SPAs, also enumerate API response handlers. Using Burp’s embedded browser, enable the DOM Invader extension. It injects a canary and highlights where sources flow to sinks, including web-message (postMessage) channels and prototype-pollution paths.[^6][^11][^12][^13][^16][^18][^20]

Step B: Identify sink context and browser behavior. For each sink, confirm what executes. MDN’s 2025 updates are explicit: insertAdjacentHTML parses as HTML and is an injection sink; document.write remains a classic execution path; innerHTML is dangerous but won’t execute <script> and will block some event handlers that used to work in older bypasses.[^7][^8][^10] For Shadow DOM, ShadowRoot.innerHTML behaves similarly to innereHTML and is sink-sensitive as well.[^14]

Step C: Payloads tailored to the sink.
Examples:

<!-- HTML sink: innerHTML -->
<div id="out"></div>
<script>
  // Vulnerable:
  const q = new URLSearchParams(location.search).get('name');
  out.innerHTML = q;  // sink
</script>
<!-- PoC -->
?name=<img src=x onerror=alert(document.domain)>
// JavaScript execution sink: setTimeout(string)
const t = new URL(location.href).hash.slice(1);
setTimeout(t, 0);
// PoC
#alert(document.domain)
<!-- document.write(), typical in legacy templates -->
<script>
  var msg = new URLSearchParams(location.search).get('m');
  document.write("<div>" + msg + "</div>");
</script>
<!-- PoC -->
?m=%3Cscript%3Ealert(document.domain)%3C/script%3E

Step D: Validate dataflow and context breaks. Use breakpoints around the sink to verify the exact string value pre-injection. Confirm post-injection DOM state in the Elements panel. For reflected DOM-XSS, ensure your value is truly reflected by the server into a client-consumed location (for example, a params object inside a script block) and only executes after client code touches it, not during HTML parse.

Step E: Stabilize the PoC. Prefer payloads that do not depend on blocked behaviors. For innerHTML, use images and onerror; for attribute contexts, break out of quotes explicitly; for URL sinks, use javascript: where applicable per framework and context, but validate the navigation/handler path actually executes (many frameworks sanitize). MDN and PortSwigger’s academy pages remain the best references for up-to-date sink behavior.[^5][^7][^8]

Reflected DOM-XSS: the hybrid your scanners miss

The following example is textbook. Server-side code emits a script block containing a JSON-ish params object built from request parameters:

<body>
  <h2 id="welcome"></h2>
  <script>
    const params = {
      bgcolor: "<%= bgcolor %>",
      name:    "<%= name %>",
      loggedin:"<%= loggedin %>"
    };
  </script>
  <script src="/static/app.js"></script>
</body>

app.js then does:

document.body.style.backgroundColor = params.bgcolor;
if (params.loggedin === "true") {
  document.getElementById("welcome").innerHTML = "Welcome " + params.name; // sink
}

The PoC is a request with a DOM-safe payload that thrives in innerHTML:

https://www.example.test/entry?bgcolor=blue&name=<img%20src=x%20onerror=alert(42)>&loggedin=true

Execution does not occur while parsing the HTML response (so it is not classic reflected XSS). It occurs only after the client script concatenates params.name into innerHTML. That is Reflected DOM-XSS. In practice, you will also encounter the API reflection variant: client code reads a JSON response with your parameter echoed, then writes it to a sink. Treat both as DOM-XSS and tune the payload to the sink.

Process notes.

  1. In Burp’s browser, enable DOM Invader, copy the canary, and inject it into the target name parameter to auto-trace the flow to innerHTML.[^11][^16][^20]
  2. If the server template encodes double quotes, switch to single quotes or use attribute/context payloads (">…, '>…) to break out correctly.
  3. Confirm that the exploit requires the client script’s execution; i.e., if you disable JavaScript and reload, the payload should not execute.

Stored DOM-XSS: server storage and client storage

Server-stored path. The attacker stores a string (via profile, comment, log, message, or support ticket). Later, the application retrieves it and renders it via client code into a sink. Your PoC sequencing must reflect this: write payload → trigger the view that causes client code to inject it.

Client-stored path. Client scripts sometimes cache user input in localStorage/sessionStorage/cookies. If code later reads those values and writes them into a sink, you have stored DOM-XSS with browser-scoped persistence. An attack may chain social engineering to stage the payload (for example, a link that executes localStorage.setItem('greeting', '<img src=x onerror=alert(1)>') in a different area), then a later view reads greeting into innerHTML.

Example (client storage):

// app.js
const greet = localStorage.getItem('greeting'); // attacker-controlled
if (greet) {
  document.querySelector('#banner').insertAdjacentHTML('beforeend', greet); // sink
}

Staging link (attacker-controlled):

javascript:localStorage.setItem('greeting','<img src=x onerror=alert(document.domain)>')

Why this is relevant. CVEs continue to identify DOM-XSS in production software.[^1][^3][^4][^15][^17] These cases frequently involve data that is either reflected and then consumed by client code, or retrieved from storage and injected into DOM sinks.

SPA, fragments, and message channels

Fragment identifiers are important: anything after # does not go to the server, so WAFs and server logs never see it. Many SPAs parse location.hash and map key–value pairs into component props. Test both location.hash and location.search.

postMessage channels. As PortSwigger documents, DOM Invader now includes dedicated features to list and replay web messages, enabling systematic testing of message handlers that route data into sinks.[^11][^12] If you see window.addEventListener('message', ...) or libraries like post-robot, treat the message payload as a source and trace its path.

Framework-specific realities

  • React. dangerouslySetInnerHTML is as risky as innerHTML. Also watch for libraries that smuggle HTML via props into components that call dangerouslySetInnerHTML. The OWASP cheat sheet and current ecosystem guidance caution on javascript:/data: URLs and other escape hatches.[^9]

  • Angular (including legacy AngularJS). Be aware of $sce bypasses and template behaviors. When you see trust APIs (bypassSecurityTrustAs*), verify that the trust boundary is not attacker-controlled.

  • jQuery. The $() selector, .html(), .attr() remain sinks when fed with attacker data, even as some historical selector injections were mitigated in newer versions.[^5]

  • Shadow DOM. The same sink dangers apply within a ShadowRoot.[^14]

Defense context for PoC discussion

You will often be asked about mitigations in reports. Cite platform references that align with your PoC:

  • Context-appropriate encoding on the server for values that will be embedded in JS vs. HTML, to avoid double-encode issues your deck mentions.

  • Safe sinks on the client (textContent, innerText) and Trusted Types to gate access to HTML/JS/URL sinks. Trusted Types policies formalize sanitization (for example, DOMPurify for HTML) and reduce accidental re-introductions of raw strings.[^8][^12]

  • CSP helps but should be framed as a defense-in-depth layer, not a sole mitigation. Your PoCs should still land without inline scripts when sink behavior allows it.

Insights and Recommendations

  1. Use taxonomy to drive coverage. Start every assessment with a category-to-path matrix. For each feature, ask: can I inject now and execute later (stored or stored DOM-XSS)? can I inject now and require client code to execute (reflected DOM-XSS)? does this page read from fragment, search, cookies, or messages (pure DOM-XSS)? This forces you to test both server and client paths and avoids “reflected-only” tunnel vision.

  2. Adopt a sink-first payload library. Maintain a small, sink-validated payload set:

    • For innerHTML/Shadow DOM HTML sinks: <img src=x onerror=/*payload*/>, <iframe srcdoc=…> patterns that do not rely on <script>.
    • For JavaScript execution sinks: hash payloads (#alert(1)), setTimeout('/*payload*/') routes, and Function() harnessing.
    • For URL/attribute sinks: quoted and unquoted breaks, explicit javascript: testing when handlers are invoked by user interaction.
  3. Instrument with DOM Invader and devtools together. DOM Invader provides canary injection, message tracing, prototype-pollution checks, and DOM-clobbering tests; use it to map flows, then confirm with breakpoints in real code paths.[^6][^11][^12][^13][^16][^18][^20]

  4. Target SPAs and storage deliberately. Always test location.hash as a source. Enumerate localStorage/sessionStorage keys and sniff where they are read. Try cross-view triggers: a payload staged in one view that executes in another is a classic stored DOM-XSS pattern.

  5. Anchor claims in recent, public examples. When writing up, reference CVEs that mirror your PoC flow (for example, DOM-XSS in AEM or recent plugin advisories) to demonstrate modern exploitability to stakeholders.[^1][^2][^3][^4][^15][^17]

  6. Deliver defensive guidance that matches your PoC. If you abused insertAdjacentHTML, recommend Trusted Types + DOMPurify for HTML sinks and conversion to textContent where possible, not a generic “add CSP.” Use OWASP’s updated cheat sheet language to back your recommendation.[^9]

Conclusion

Thinking in refined categories converts “maybe exploitable” into reproducible PoCs that stand up in triage. In 2025, verified advisories still show DOM-XSS through the usual suspects: URL/fragment sources, API reflections, and client storage being funneled into HTML and JavaScript sinks. The combination of a sink-aware payload library, DOM Invader’s canary tracing, and disciplined devtools debugging reliably uncovers exploitable paths that server-side scanners miss. Going forward, expect more message-channel bugs and complex client chains; keep building PoCs that reflect these realities and recommend mitigations aligned to sinks, not generic platitudes. What hybrid path—reflected DOM or stored DOM—has been the highest yield for you lately, and which sink gave you the best leverage?

Key Takeaways

  • Build PoCs per sink. Use <img onerror> or iframe tricks for innerHTML and insertAdjacentHTML; reserve <script> for document.write and true JS execution sinks.
  • Treat fragments, storage, and web messages as first-class sources. Trace them with DOM Invader and confirm with devtools breakpoints.
  • Always test hybrid paths: reflected-to-DOM and stored-to-DOM. These win where traditional reflected/stored checks pass.
  • Align mitigations to sinks: convert to textContent, enforce Trusted Types with a sanitizer (DOMPurify), and use context-aware server encoding for values embedded into JS vs. HTML.

[^1]: Adobe Security Bulletin (multiple DOM-XSS and stored XSS issues, July–August 2025). https://helpx.adobe.com/security/products/experience-manager/apsb25-48.html
[^2]: NVD: CVE-2025-47053 (AEM DOM-XSS, July 16, 2025). https://nvd.nist.gov/vuln/detail/CVE-2025-47053
[^3]: NVD: CVE-2025-58618 (DOM-XSS in Pie Calendar, September 3, 2025). https://nvd.nist.gov/vuln/detail/CVE-2025-58618
[^4]: NVD: CVE-2025-58631 (DOM-XSS in IssueM, September 3, 2025). https://nvd.nist.gov/vuln/detail/CVE-2025-58631
[^5]: PortSwigger Web Security Academy – DOM-based XSS (sources, sinks, hybrid reflected/stored DOM-XSS). https://portswigger.net/web-security/cross-site-scripting/dom-based
[^6]: PortSwigger – DOM Invader (Last updated July 17, 2025). https://portswigger.net/burp/documentation/desktop/tools/dom-invader
[^7]: MDN – Element.insertAdjacentHTML (XSS sink, Aug 11, 2025). https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
[^8]: MDN – Document.write (XSS considerations, Aug 12, 2025). https://developer.mozilla.org/en-US/docs/Web/API/Document/write
[^9]: OWASP Cheat Sheet – Cross-Site Scripting Prevention (framework escape hatches, React/Angular items). https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
[^10]: Feroot – What is DOM-based XSS? (June 18, 2025). https://www.feroot.com/education-center/what-is-dom-based-xss/
[^11]: PortSwigger – Testing for DOM XSS using web messages (updated July 17, 2025). https://portswigger.net/burp/documentation/desktop/tools/dom-invader/web-messages
[^12]: MDN – Trusted Types API (May 27, 2025). https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API
[^13]: PortSwigger – Testing for client-side prototype pollution with DOM Invader (updated July 17, 2025). https://portswigger.net/burp/documentation/desktop/tools/dom-invader/prototype-pollution
[^14]: MDN – ShadowRoot.innerHTML (Aug 20, 2025). https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/innerHTML
[^15]: NVD: CVE-2025-49424 (DOM-XSS in Visual Composer component, Aug 20, 2025). https://nvd.nist.gov/vuln/detail/CVE-2025-49424
[^16]: PortSwigger – DOM Invader attack types (updated July 17, 2025). https://portswigger.net/burp/documentation/desktop/tools/dom-invader/settings/attack-types
[^17]: NVD: CVE-2025-46856 (AEM DOM-XSS, Aug 20, 2025). https://nvd.nist.gov/vuln/detail/CVE-2025-46856
[^18]: PortSwigger – Testing for DOM clobbering with DOM Invader (updated July 17, 2025). https://portswigger.net/burp/documentation/desktop/tools/dom-invader/dom-clobbering
[^20]: PortSwigger – Testing for DOM XSS with DOM Invader (step-by-step, published last week). https://portswigger.net/burp/documentation/desktop/testing-workflow/input-validation/xss/dom-xss