Let’s start with a confession: we’ve all fallen for that 'rookie' excitement at some point. I was in the early years of my career, conducting a pentest for an e-commerce platform. I noticed a URL parameter reflecting directly onto the page. 'Alright,' I thought, 'I've got this.' I was so confident that instead of a simple script, I pasted a massive payload designed to stress the browser and manipulate every possible DOM element. The result? I found the vulnerability, sure, but that massive JS load broke the Single Page Application (SPA) state management in the client's live environment. Everyone visiting the site was met with a blank white screen. For about 45 minutes, I worked alongside 'testCompany' engineers to untangle that knot. The biggest lesson I learned that day: The browser is a much more dangerous playground than we think.
The Server is Safe Now (Or is it?)
For years, we’ve been shouting about 'input validation.' Nowadays, most frameworks (React, Vue, Angular) provide a decent level of protection against server-side attacks. But the real danger has quietly shifted. While everything used to happen on the PHP or Java side, the logic has now moved to the client—the user's browser. This is exactly where DOM-based Cross-Site Scripting (XSS) comes into play.
In a standard XSS attack, the payload travels to the server and comes back. But in a DOM-based one, the server doesn't even have a clue. Everything happens within client-side objects like window.location.hash. When you look at the server logs, everything looks squeaky clean, but the user's world has already been turned upside down.
Sources and Sinks
To grasp the logic of DOM XSS, you need to know two concepts very well: Source and Sink.
A Source is data that an attacker can control. Examples include: location.search, location.hash, document.referrer, and even localStorage.
A Sink is the dangerous function where this data is processed and 'executed' by the browser. The classic culprits are: eval(), setTimeout(), setInterval(), and of course, our favorite, element.innerHTML.
Imagine a scenario where the testCompany site has a greeting screen addressing the user by name:
// Incorrect and dangerous usage
const urlParams = new URLSearchParams(window.location.search);
const userName = urlParams.get('name');
const welcomeMessage = document.getElementById('welcome-msg');
// This is the line that invites disaster
welcomeMessage.innerHTML = "Welcome, " + userName;
What happens if an attacker crafts a URL like this?
https://example.com/dashboard?name=<img src=x onerror=alert(document.domain)> (Defanged)
When the browser processes this URL, it sends the ?name=... part to the server, but as modern browsers and JS engines render the page, they add that <img> tag to the DOM tree via innerHTML. Since src=x is invalid, the onerror event is triggered—and bingo! Our code starts running in the user's browser with the privileges of that site.
Why WAFs Don't Save Us?
Many security teams think, 'In front of our...
