Skip to main content
Educational Purpose Only - XSS attacks can steal user sessions, deface websites, and distribute malware. Only practice on authorized systems.

Overview

Cross-Site Scripting (XSS) is a vulnerability that allows attackers to inject malicious JavaScript code into web pages viewed by other users. This demo contains a Reflected XSS vulnerability where user input from URL parameters is rendered directly in the page without sanitization.

Severity Rating

Vulnerable Code

The vulnerability exists in both the Flask backend and Jinja2 template:
@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        return redirect('/login')
    
    # VULNERABLE: No sanitization of user input
    message = request.args.get('message', '')
    
    return render_template('dashboard.html', 
                         username=session.get('username'),
                         message=message,  # Passed directly to template
                         role=session.get('role'))

Why This Is Dangerous

  1. No Input Sanitization: The message parameter from the URL is passed directly to the template without any filtering or encoding
  2. Jinja2 |safe Filter: The |safe filter explicitly disables Jinja2’s automatic HTML escaping (dashboard.html:14)
  3. User-Controlled Content: Attackers can craft malicious URLs containing JavaScript payloads
  4. Reflected Immediately: The payload executes as soon as the victim loads the URL

Exploitation Steps

1

Authenticate to the Application

First, log into the vulnerable application:
URL: https://auth-vulnerable.onrender.com/login
Username: admin
Password: admin123
This is necessary because the dashboard route requires authentication (vulnerable/app.py:77).
2

Craft XSS Payload

Create a malicious URL with JavaScript in the message parameter:
https://auth-vulnerable.onrender.com/dashboard?message=<script>alert('XSS')</script>
Result: Popup alert appears, confirming JavaScript execution
3

Deliver Payload to Victim

In a real attack scenario, the attacker would:
  1. Craft the malicious URL with the XSS payload
  2. Encode the URL to bypass basic filters:
    encodeURIComponent('<script>alert(1)</script>')
    // Result: %3Cscript%3Ealert(1)%3C%2Fscript%3E
    
  3. Deliver via:
    • Phishing emails
    • Social media messages
    • Forum posts
    • Shortened URLs (bit.ly, etc.)
  4. Wait for victim to click and execute the payload

Impact Analysis

Confirmed in Demo

  • Arbitrary JavaScript execution
  • Access to session cookies
  • DOM manipulation
  • Page defacement
  • Keylogging capability

Real-World Risks

  • Session hijacking (cookie theft)
  • Credential harvesting
  • Malware distribution
  • Phishing attacks
  • Cryptocurrency miners
  • Worm propagation

Types of XSS

Characteristics:
  • Payload comes from HTTP request (URL, form submission)
  • Immediately reflected back in the response
  • Requires victim to click a malicious link
  • Not stored in the database
Example: The message parameter in our demo
message = request.args.get('message', '')  # From URL
return render_template('page.html', message=message)  # Immediately rendered
Characteristics:
  • Payload stored in database
  • Executes every time the page is loaded
  • More dangerous (affects all users)
  • No user interaction required after storage
Example:
# Vulnerable profile update
bio = request.form['bio']  # Contains <script>evil</script>
cursor.execute("UPDATE users SET bio = ? WHERE id = ?", (bio, user_id))
# Later, when profile is viewed:
# return render_template('profile.html', bio=user.bio|safe)  # VULNERABLE
Characteristics:
  • Payload never reaches the server
  • Entirely client-side JavaScript vulnerability
  • Harder to detect with server-side tools
Example:
// Vulnerable client-side code
var message = window.location.hash.substring(1);
document.getElementById('output').innerHTML = message;  // VULNERABLE

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

Secure Implementation

The secure version properly handles XSS prevention:
from markupsafe import escape

@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        return redirect('/login')
    
    # SECURE: HTML escaping
    message = request.args.get('message', '')
    
    return render_template('dashboard.html', 
                         username=session.get('username'),
                         message=escape(message),  # Escaped before rendering
                         role=session.get('role'))

How the Fix Works

1

HTML Entity Encoding

The escape() function converts special characters:
from markupsafe import escape

escape('<script>alert(1)</script>')
# Returns: '&lt;script&gt;alert(1)&lt;/script&gt;'
CharacterEncoded
<&lt;
>&gt;
"&quot;
'&#x27;
&&amp;
2

Jinja2 Auto-Escaping

Without the |safe filter, Jinja2 automatically escapes HTML:
<!-- Secure: Auto-escaped -->
{{ message }}

<!-- Vulnerable: Escaping disabled -->
{{ message|safe }}
3

Content Security Policy (CSP)

CSP headers restrict JavaScript execution:
response.headers['Content-Security-Policy'] = "default-src 'self'"
This prevents:
  • Inline <script> tags
  • eval() and new Function()
  • Inline event handlers (onclick, onerror, etc.)
  • Loading scripts from external domains

Mitigation Strategies

Always encode user input before rendering:
# Python/Flask
from markupsafe import escape
safe_output = escape(user_input)

# JavaScript
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#x27;'
    };
    return text.replace(/[&<>"']/g, m => map[m]);
}
Implement strict CSP headers:
# Strict CSP
response.headers['Content-Security-Policy'] = (
    "default-src 'self'; "
    "script-src 'self'; "
    "style-src 'self' 'unsafe-inline'; "
    "img-src 'self' data:; "
    "font-src 'self'; "
    "connect-src 'self'; "
    "frame-ancestors 'none';"
)
Validate and sanitize input:
import re

def validate_message(message):
    # Remove HTML tags
    clean = re.sub(r'<[^>]*>', '', message)
    
    # Limit length
    if len(clean) > 200:
        return None
    
    # Whitelist allowed characters
    if not re.match(r'^[a-zA-Z0-9\s.,!?-]+$', clean):
        return None
    
    return clean
Prevent JavaScript access to cookies:
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
This prevents document.cookie from accessing session tokens.
Use template auto-escaping:
# Flask/Jinja2: Auto-escaping enabled by default
# NEVER use |safe unless absolutely necessary

# React: Escapes by default
<div>{userInput}</div>

# Vue: Use v-text instead of v-html
<div v-text="userInput"></div>

Testing and Detection

Manual Testing Payloads

<!-- Basic test -->
<script>alert(1)</script>

<!-- Event handlers -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>

<!-- Bypass filters -->
<ScRiPt>alert(1)</ScRiPt>
<script>alert(String.fromCharCode(88,83,83))</script>

<!-- Without script tags -->
<img src="javascript:alert(1)">
<iframe src="javascript:alert(1)">

<!-- DOM-based -->
<img src=x onerror="eval(atob('YWxlcnQoMSk='))">

Automated Tools

XSS Strike

python3 xsstrike.py -u "http://target.com/page?param=test"

Burp Suite

  • Scanner: Automated XSS detection
  • Intruder: Custom payload testing
  • Repeater: Manual verification

OWASP ZAP

zap-cli quick-scan --spider \
  -r "http://target.com"

DOMPurify

JavaScript sanitization library:
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty);

References

Next Steps

Learn about related vulnerabilities:

Build docs developers (and LLMs) love