October 27, 2026
XSS Hands-On: Reflected, Stored, and DOM-Based Cross-Site Scripting
Cross-Site Scripting (XSS) is one of the most common web vulnerabilities, and it comes in three main flavours. In this writeup I walk…
By Loay Essam
2 min read
Cross-Site Scripting (XSS) is one of the most common web vulnerabilities, and it comes in three main flavours. In this writeup I walk through all three, reflected, stored, and DOM-based, from my own hands-on labs, and explain how they differ and why they matter.
What XSS actually is
XSS happens when a website takes input from a user and places it into a page without properly treating it as text. The browser then runs that input as HTML or JavaScript. In short, my data becomes the page's code, and any script I inject runs in the victim's browser with their session, able to steal cookies, perform actions as them, or deface the page.
Reflected XSS
Reflected XSS is the simplest. My input is sent in a request, for example a search box or a URL parameter, and the server immediately reflects it back on the results page without encoding it. The payload is not stored anywhere; it only fires for whoever sends that exact request, so an attacker has to trick a victim into clicking a crafted link.
<script>alert(document.domain)</script><script>alert(document.domain)</script>If the page prints my input straight into the HTML, this script runs and pops an alert showing the site's domain, the classic proof of concept. A real attacker would swap the alert for code that steals the session cookie and sends it to their own server.
Stored XSS
Stored XSS is more dangerous. Here my payload is saved by the application, in a comment, a profile field, or a message, and served to every user who later views that page. I do not need to trick anyone into clicking a link; the victims come to the page themselves, and the script runs for each of them. In my lab I posted a comment containing a script, and it executed for anyone who opened the page.
<script>alert('Stored XSS')</script><script>alert('Stored XSS')</script>Because it is stored and automatic, this version can spread on its own and hit many users, including administrators, which makes it the highest impact of the three.
DOM-based XSS
DOM-based XSS is different because the server is never involved. The vulnerability lives entirely in the page's own JavaScript, which reads something from the page (like the part of the URL after the # symbol) and writes it into the page unsafely, for example with innerHTML. The malicious data may never even reach the server, so server-side filters do not see it.
#<img src=x onerror=alert('DOM XSS')>#<img src=x onerror=alert('DOM XSS')>Here the img tag fails to load its fake source x, which triggers the onerror handler and runs my JavaScript. Because the # fragment never leaves the browser, this is a purely client-side bug.
How the three differ
The key difference is where the input is handled and who gets hit. Reflected XSS bounces my input straight back from the server and only affects the single victim who opens my crafted link. Stored XSS is saved on the server and runs for every visitor to the page, with no link needed. DOM-based XSS never involves the server at all; it is caused by unsafe JavaScript in the browser. Reflected and stored are server-side issues, while DOM-based is purely client-side.
How to defend against it
The main defence is output encoding: whenever user input is placed into a page, encode it for its context so the browser shows it as text instead of running it. Validate input on the server, and for DOM XSS avoid dangerous sinks like innerHTML in favour of safe ones like textContent. A strong Content Security Policy adds a second layer by blocking inline scripts even if a payload slips through.
What I learned
Working through all three types made the pattern click. XSS is really one idea, input that gets treated as code, and the three types are just different delivery routes: reflected through a link, stored through the database, and DOM-based through the browser's own JavaScript. Once I understood that the fix is always the same, treat input as data and encode it on output, the payloads stopped feeling like magic and started making sense. Seeing an alert box pop from my own input is also a great reminder of how much trust a browser places in whatever a page tells it to run.