XSS Remediation

Risk Severity
๐ŸŸ  High
Fix Effort
๐Ÿ”ง Medium
Est. Time
โฑ๏ธ 1-3 hours
Reference
A03:2021 CWE-79

Cross-Site Scripting (XSS) flaws occur whenever an application includes untrusted data in a new web page without proper validation or escaping. This allows attackers to execute malicious scripts in the victim's browser.

XSS Impact

XSS allows attackers to hijack user sessions, steal credentials, redirect users to malicious sites, deface websites, and perform actions as the victim. Stored XSS is particularly dangerous as it affects all users who view the compromised content.

๐Ÿ“‘ Table of Contents

๐Ÿ›ก๏ธ Primary Defenses

๐Ÿงช Validation & Testing

โš ๏ธ Pitfalls to Avoid

Stored XSS

Malicious script is permanently stored on the target server (database, comments, profiles). Affects all users who view the data.

Reflected XSS

Script is reflected off the server (error messages, search results). Requires victim to click a crafted link.

DOM-based XSS

Vulnerability exists in client-side JavaScript. Payload never reaches the server. Harder to detect with server-side tools.

Output Encoding

The primary defense against XSS is context-sensitive output encoding. Convert untrusted input into a safe form where the input is displayed as data to the user without executing as code in the browser.

๐Ÿ“ Context Matters

Different contexts require different encoding. HTML encoding (< โ†’ &lt;) is correct for HTML body content but wrong for JavaScript strings or URLs. Using the wrong encoding can leave you vulnerable or break functionality.

Python (Flask/Jinja2)

Auto-escaping is enabled by default. In templates, use {{ variable }} which is automatically escaped. Only use {{ variable|safe }} when you've sanitized content.

For manual encoding:

python
from markupsafe import escape
safe_output = escape(user_input)

JavaScript (DOM Manipulation)

Vulnerable:

javascript
element.innerHTML = userInput;
document.write(userInput);

Secure (Text Content):

javascript
element.textContent = userInput;

Secure (HTML Content with DOMPurify):

javascript
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;

React

JSX auto-escapes by default:

jsx
return <div>{userInput}</div>;

Dangerous (Avoid):

jsx
return <div dangerouslySetInnerHTML={{__html: userInput}} />;

PHP

HTML Context:

php
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

JavaScript Context:

php
echo json_encode($userInput, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);

URL Context:

php
echo urlencode($userInput);

Node.js (Express with EJS)

Use <%= %> for escaped output (safe). Avoid <%- %>.

html
<p><%= userInput %></p>

Content Security Policy (CSP)

CSP is a defense-in-depth mechanism that can significantly reduce the impact of XSS attacks by restricting the sources of executable scripts.

๐Ÿ›ก๏ธ How CSP Works

CSP tells the browser exactly which resources are allowed to load and execute. Even if an attacker injects <script> tags, the browser will refuse to execute them if they violate the policy. It's like a firewall for your page's content.

Strong CSP Header

This policy allows scripts only from your own domain, blocks inline scripts (the primary XSS vector), and prevents your page from being framed.

http
Content-Security-Policy: 
    default-src 'self';
    script-src 'self';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    font-src 'self';
    connect-src 'self';
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';

Express.js (Helmet)

javascript
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        connectSrc: ["'self'"],
        frameAncestors: ["'none'"],
    }
}));

Flask (Talisman)

python
from flask_talisman import Talisman

csp = {
    'default-src': "'self'",
    'script-src': "'self'",
    'style-src': "'self' 'unsafe-inline'",
}
Talisman(app, content_security_policy=csp)

Django (django-csp)

python
# In settings.py
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")

Web Server Config

Apache (.htaccess):

apache
Header set Content-Security-Policy "default-src 'self'; script-src 'self'"

Nginx:

nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";

๐Ÿงช Testing Verification

Use these payloads to verify your XSS protections are working. Test in all input fields and URL parameters.

XSS Test Payloads

html
# Basic XSS tests - should be encoded, not execute
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg onload=alert('XSS')>

# Attribute injection tests
" onmouseover="alert('XSS')
' onclick='alert(1)'

# JavaScript URL scheme
javascript:alert('XSS')

# Template literal injection (for JS contexts)
${alert('XSS')}

# Expected result: All payloads should display as text, NOT execute

CSP Validation

bash
# Check CSP header is present
curl -I https://yoursite.com | grep -i "content-security-policy"

# Test inline script blocking (should fail with CSP)
# Add this to a page - it should NOT execute with proper CSP
<script>console.log('inline script')</script>

# Browser DevTools: Check Console for CSP violation reports
# Look for: "Refused to execute inline script..."

โš ๏ธ Common Mistakes

โŒ innerHTML with User Data

javascript
// DON'T: Direct HTML assignment
element.innerHTML = userInput;
document.write(userInput);

Allows arbitrary HTML/script injection

โœ… Correct Approach

javascript
// DO: Use textContent or sanitize
element.textContent = userInput;
// or with sanitizer
element.innerHTML = DOMPurify.sanitize(userInput);

textContent treats everything as text

โŒ Blacklist-Based Filtering

python
# DON'T: Easy to bypass
def sanitize(input):
    return input.replace('<script>', '')

Easily bypassed with <SCRIPT> or <scr<script>ipt>

โœ… Correct Approach

python
# DO: Use proper encoding
from markupsafe import escape
safe = escape(user_input)  # Encodes ALL special chars

Encoding is foolproof, filtering is not

โŒ Weak CSP with unsafe-inline

http
Content-Security-Policy: 
  script-src 'self' 'unsafe-inline';

unsafe-inline defeats the purpose of CSP

โœ… Correct Approach

http
Content-Security-Policy: 
  script-src 'self' 'nonce-abc123';

Use nonces or hashes for inline scripts