XSS Remediation
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
๐ 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 (< โ <) 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:
from markupsafe import escape
safe_output = escape(user_input)JavaScript (DOM Manipulation)
Vulnerable:
element.innerHTML = userInput;
document.write(userInput);Secure (Text Content):
element.textContent = userInput;Secure (HTML Content with DOMPurify):
const clean = DOMPurify.sanitize(userInput);
element.innerHTML = clean;React
JSX auto-escapes by default:
return <div>{userInput}</div>;Dangerous (Avoid):
return <div dangerouslySetInnerHTML={{__html: userInput}} />;PHP
HTML Context:
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');JavaScript Context:
echo json_encode($userInput, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);URL Context:
echo urlencode($userInput);Node.js (Express with EJS)
Use <%= %> for escaped output (safe). Avoid <%- %>.
<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.
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)
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)
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)
# In settings.py
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")Web Server Config
Apache (.htaccess):
Header set Content-Security-Policy "default-src 'self'; script-src 'self'"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
# 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 executeCSP Validation
# 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
// DON'T: Direct HTML assignment
element.innerHTML = userInput;
document.write(userInput);Allows arbitrary HTML/script injection
โ Correct Approach
// DO: Use textContent or sanitize
element.textContent = userInput;
// or with sanitizer
element.innerHTML = DOMPurify.sanitize(userInput);textContent treats everything as text
โ Blacklist-Based Filtering
# DON'T: Easy to bypass
def sanitize(input):
return input.replace('<script>', '')Easily bypassed with <SCRIPT> or <scr<script>ipt>
โ Correct Approach
# DO: Use proper encoding
from markupsafe import escape
safe = escape(user_input) # Encodes ALL special charsEncoding is foolproof, filtering is not
โ Weak CSP with unsafe-inline
Content-Security-Policy:
script-src 'self' 'unsafe-inline';unsafe-inline defeats the purpose of CSP
โ Correct Approach
Content-Security-Policy:
script-src 'self' 'nonce-abc123';Use nonces or hashes for inline scripts