Cross-Site Scripting (XSS): Types, Impact, and Detection
"Just use React/Vue/Svelte, it handles escaping for you."
If I had a dollar for every time I heard that, I could retire and buy a small island. It's the most common misconception in frontend security. Yes, modern frameworks are better than the jQuery spaghetti we wrote in 2015, but XSS (Cross-Site Scripting) is very much alive.
In fact, as our frontends have become more complex, the vectors for XSS have just gotten weirder.
The Three Flavors of XSS
If you're new to this, or just need a refresher, XSS usually falls into three buckets.
1. Reflected XSS
This is the classic phishing setup. An attacker sends a link: example.com/search?q=<script>alert(1)</script>. The server takes that q parameter and pastes it right back into the HTML of the page saying "You searched for: [payload]". The victim's browser sees the script and runs it.
Fix: Don't trust URL parameters.
2. Stored XSS
This is the nasty one. The attacker posts a comment on a forum or updates their profile with a malicious script. The server saves it. Now, every single person who views that profile loads the script. This is how worms spread.
Fix: Sanitize on input, escape on output.
3. DOM-Based XSS
This is where 2026 apps fail the most. The server is fine. The database is fine. But some JavaScript on the client side takes data from the URL (like window.location.hash) and innerHTML's it into a div without checking it. The traffic never hits the server, so your server-side firewall (WAF) never sees it.
The "Dangerously" Trap
React is great because it escapes content by default. But sometimes, you need to render HTML that comes from a CMS or a Markdown parser.
So React gives you dangerouslySetInnerHTML. The name is literally a warning sign. Yet, I see it in code reviews all the time. Developers get lazy. They want to render a bold tag, so they bypass the entire security model of the framework.
If you use this function, you have to scrub that HTML clean first. Use a library like DOMPurify. If you don't, you are handing the keys to the kingdom to anyone who can inject content into that string.
Why Context Matters
Here is the thing about sanitization: it depends on where the data is going.
Putting user input into a <div> is different than putting it into an <a href="...">. If I let a user set their website URL, and I put it into an anchor tag:
<a href="${userUrl}">Website</a>And the user sets their URL to: javascript:stealCookies().
I have just created an XSS vulnerability, even if I stripped out all the <script> tags. The context (an href attribute) allows for protocol execution. You need to validate that the URL starts with http:// or https://.
Catching it with AI
XSS is hard to hunt manually because modern apps are massive trees of components. Tracking a piece of data from a URL parameter down through six layers of props into a specialized UI component is mentally exhausting.
This is where AI-driven scanning shines. In the VigilFlux pipeline, we use scanners that can "read" the component tree. They understand that a prop named htmlContent passed into a specific component eventually lands in a raw HTML sink.
We also use dynamic analysis (DAST). We spin up a headless browser, log into the app, and crawl it like a user. The scanner tries to inject harmless "canary" tokens into fields and watches to see if those tokens ever pop up as executable code in the browser.
The Safety Net: CSP
Finally, you need a Content Security Policy (CSP).
Think of CSP as a second firewall that lives in the browser. It tells the browser: "Only load scripts from mysite.com and analytics.google.com. Block everything else."
If an attacker manages to inject a script that tries to load malware from evil-hacker.com, the browser will look at the CSP header, see that the domain isn't on the list, and block the request.
It's annoying to set up. You will break your analytics at least twice. But it is the single most effective mitigation against XSS when your code fails.
XSS isn't going away. As long as we allow users to input data and we show that data to other users, the risk exists. Stop trusting your framework to do all the heavy lifting.