Skip to content
Categoria: Pentest8 min read

Modern XSS: DOM, Stored and Reflected With Real Examples in a Test Lab

Por Lucas Andrade ·

Three XSS flavors dissected in a sandbox with payloads, exploitation flow, and mitigations via strict CSP, Trusted Types and DOMPurify sanitization.

Modern XSS: DOM, Stored and Reflected With Real Examples in a Test Lab

HackerOne's 2025 report ranked XSS as the second most reported bug across public bounties, with a median payout of USD 750 and outliers near USD 20k on enterprise targets. The bug class refuses to die because browsers keep evolving while frontend pipelines still glue strings into innerHTML without ceremony. The Basilisk team spins up a lab with three intentionally vulnerable apps, captures every request through Burp Suite, and walks each vector with payload, context, and patch. Before you copy any payload, confirm your lab is isolated, because firing scripts at third parties without written scope is still a crime under the US CFAA and the UK Computer Misuse Act.

The three XSS families at a glance

XSS is the injection and execution of attacker JavaScript in the security context of someone else's origin. Three families cover practically every case: Reflected (the payload arrives in the request and is echoed straight back), Stored (the payload persists in the backend and hits every future visitor), and DOM-based (the injection happens entirely client-side, with the server never seeing the payload). The impact is the same in all three: the attacker inherits the victim's privileges in the browser, can steal session tokens, act on the user's behalf, and, against an admin, take over the whole application.

Reflected XSS

Reflected XSS shows up when user input lands back in the HTTP response without proper encoding, usually through a querystring or GET form. In our lab a search at /search?q= drops the term inside an h2, so the classic <svg/onload=alert(document.domain)> fires at top-level context. What separates an amateur report from a professional one is proving impact: exfiltrating the session cookie via fetch to an attacker domain only works when the cookie is not HttpOnly. Capture the gap in Burp's history pane, with the exact request and response context, because a report without a reproducible request lands in the duplicate or informative pile.

Stored XSS

Stored XSS is the nastiest variant because it sits in the database and hits every future visitor. We dial DVWA to medium, paste <img src=x onerror=...> into the comments field, and watch the webhook collect moderator sessions within seconds as the cookie is base64-encoded and sent out. Real-world surface includes Markdown renderers, transactional email templates, CSV exports opened inside Excel, and even EXIF metadata parsed by an internal dashboard. Robust defense layers input sanitization with output escaping; one without the other is theater, because a cleanly stored value still fires when it lands in the wrong context.

DOM-based XSS

DOM-based XSS happens entirely in the browser, so the payload never touches the server logs. The classic location.hash piped into document.write still ships in legacy chat widgets and in React apps that hand hash data to dangerouslySetInnerHTML. We adapt the Google XSS Game level 1 inside the lab and demonstrate sink hunting by opening DevTools, toggling 'Pause on exceptions', and unleashing Burp's DOM Invader extension. The triage flow is always the same: pinpoint the source (hash, search, postMessage), trace it to the sink (innerHTML, eval, setAttribute), validate with a minimal payload, then escalate. Because the server sees nothing, server-side WAFs are blind here.

Context is everything: correct escaping

The same payload fires or fails depending on the injection context. HTML body needs HTML-entity encoding, an attribute value adds quote handling, a <script> block needs JavaScript string escaping, a URL needs context-aware URL encoding, and a style context has its own rules. The root of almost every XSS is a value that crosses from one context into another without being re-encoded. That is why the OWASP rule of thumb is: encode on output, matched to the context, never filter generically on input. A blacklist filter that only strips angle brackets falls instantly to attribute-based vectors like onmouseover.

Building the lab

Stand up the lab today with DVWA, OWASP Juice Shop, and a stripped Next.js app, all in Docker containers on an isolated network with no route to the internet except a controlled webhook for the exfil proof. Route all browser traffic through Burp, enable proxy history logging, and build a Repeater collection per vector. Reproduce all three vectors until each scores a popup, and note for each the exact request, the injection context, and the response snippet. This runbook is later the backbone of clean bug bounty reports with one-sentence reproduction.

Modern mitigation: CSP, Trusted Types, DOMPurify

Modern mitigation stopped being about filtering angle brackets years ago. Content Security Policy level 3 with a per-request nonce kills inline injection even when an attacker squeezes HTML onto the page, provided you do not cave and add unsafe-inline as a fallback. Trusted Types turns innerHTML assignments into a type error unless they flow through a registered policy. Pair it with DOMPurify for rich HTML and Google's measurements show roughly 90% surface reduction. HttpOnly on the session cookie neutralizes the classic cookie theft, and SameSite dampens cross-origin forwarding of the credential.

Shift-left: catch XSS in CI

Wire detection into CI by running Semgrep with javascript.lang.security.audit.xss on every PR, complemented by ESLint plugins that flag dangerouslySetInnerHTML and raw innerHTML assignments. A DAST pass against the staging instance catches what static analysis misses, such as reflected parameters in third-party templates. The key is that a new sink breaks the build, not just produces a report nobody reads. That is how XSS prevention becomes part of the Definition of Done instead of an afterthought pentest.

Common pitfalls

The first pitfall is blacklisting: any filter that only blocks known payloads falls to encoding variants and alternative event handlers. The second is treating a WAF as the only defense, bypassed with case mixing, comments, and double encoding. The third is a CSP with unsafe-inline, which effectively blocks nothing. The fourth is assuming a framework like React is automatically safe while dangerouslySetInnerHTML and href injection remain wide open. The fifth is missing HttpOnly, which upgrades every reflected gap straight into session theft.

Checklist

Test every injection point across all five contexts (HTML body, attribute, script, URL, style). Confirm whether the session cookie has HttpOnly and SameSite set. Verify the CSP: no unsafe-inline, per-request nonce, restrictive script-src. Test whether Trusted Types is enforced. Prove impact with a harmless but unambiguous demonstration (a popup with document.domain or a callback carrying no real data). Document one-sentence reproduction, impact, and a suggested patch per finding, and map every finding to the STRIDE buckets Tampering and Elevation before you file the ticket.

Advanced vectors beyond the popup

An alert(1) proves injection, but a mature report shows the chain behind it. With stable XSS you can read CSRF tokens straight from the DOM and issue requests on the victim's behalf, defeating the usual CSRF defense because the request originates from the legitimate origin. Through the Fetch API the payload can query authenticated endpoints and exfiltrate responses, including profile data or admin panels. A service worker registered via XSS survives even a page reload and intercepts future traffic. Especially underrated is the combination of XSS and an open postMessage handler that accepts data across frame boundaries: a single missing origin check turns a harmless subdomain into a beachhead. That is why we never rate impact by the popup, but by the realistically reachable action: account takeover, data exfiltration, or privilege escalation to an admin, each with a demonstrated but non-harmful proof.

Reporting and responsible disclosure

A technically correct finding with no clean report fizzles out. The structure that gets triaged on HackerOne, Bugcrowd, and Intigriti is always the same: a precise summary, the affected URL and parameter, one-sentence reproduction with the exact payload, a demonstrated impact, and a concrete patch suggestion. The impact proof stays deliberately harmless: a document.domain popup or a callback carrying no real user data, never a mass exfiltration of live sessions. Screenshots and a short video clip speed triage considerably. Stick strictly to the program scope; a hit outside scope is not glory, it is legal risk. Document the CVSS vector-string rationale so the severity does not look negotiable. A report that does the defender's job of understanding and deploying the fix gets paid faster and builds the reputation that later leads to private invitations with higher bounties.

FAQ: Does a modern framework make XSS impossible?

No. React, Angular, and Vue auto-escape standard bindings, but they leave deliberate escape hatches open: dangerouslySetInnerHTML, v-html, bypassSecurityTrustHtml, and href/src attributes carrying javascript: URLs. Those hatches are the most common XSS source in modern codebases. The framework shrinks the surface but does not replace CSP, Trusted Types, and context-aware escaping.

FAQ: Is a WAF enough against XSS?

No. A WAF is a useful outer layer, but it is bypassable with encoding, case mixing, and protocol-level tricks, and it cannot see DOM-based XSS at all because the payload never reaches the server. Treat the WAF as early warning and noise reduction, never as a replacement for output escaping and a strict CSP.

Conclusion

Practical takeaway: stand up the lab, reproduce all three vectors until you score a popup on each, then bolt on nonce-based CSP, Trusted Types, and DOMPurify and replay the exact payloads. Anything that still fires deserves a policy tweak until it breaks. Track each iteration in a personal runbook because the best XSS reports require one-sentence reproduction, demonstrated impact, and a suggested patch. XSS is not a solved problem, it is a discipline: encode by context, layer defense-in-depth, and treat every new output as a potential sink until proven otherwise.

Related posts

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly