Skip to main content

Overview

Output encoding is critical for preventing Cross-Site Scripting (XSS) attacks. This guide demonstrates how to safely display user-generated content in your application.

What is XSS?

Cross-Site Scripting (XSS) occurs when an attacker injects malicious scripts into web pages viewed by other users.

Stored XSS

Malicious script is stored in the database and executed when displayed to users.

Reflected XSS

Malicious script in URL parameters is reflected back in the response.

DOM-based XSS

Client-side JavaScript manipulates the DOM with untrusted data.

Flask’s Auto-Escaping

Flask’s Jinja2 templates automatically escape variables by default, providing built-in XSS protection.

Template Auto-Escaping

<!-- Automatically escaped by Jinja2 -->
<h1>Welcome, {{ username }}</h1>

<!-- If username contains: <script>alert('XSS')</script> -->
<!-- It will be rendered as: &lt;script&gt;alert('XSS')&lt;/script&gt; -->
Jinja2 automatically escapes: <, >, &, ", and ' characters to prevent XSS attacks.

Manual Escaping with MarkupSafe

When handling user input in Python code, use escape() from MarkupSafe:
secure/app.py
from markupsafe import escape

@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        return redirect('/login')
    
    # Get user input from query parameters
    message = request.args.get('message', '')
    
    # Escape the message to prevent XSS
    return render_template('dashboard.html', 
                         username=session.get('username'),
                         message=escape(message),
                         role=session.get('role'))
1

Import escape function

from markupsafe import escape
2

Escape user input

safe_message = escape(user_input)
3

Pass to template

return render_template('template.html', message=safe_message)

Context-Specific Encoding

Different contexts require different encoding strategies:

HTML Context

Use escape() for HTML content:
safe_html = escape(user_input)

JavaScript Context

JSON encode data for JavaScript:
import json
safe_js = json.dumps(user_input)

URL Context

URL encode parameters:
from urllib.parse import quote
safe_url = quote(user_input)

CSS Context

Avoid user input in CSS; if necessary, strictly whitelist values.

Safe vs Unsafe Practices

HTML Output

Safe

from markupsafe import escape

message = escape(request.args.get('message', ''))
return render_template('page.html', message=message)
<p>{{ message }}</p>

Unsafe

message = request.args.get('message', '')
return render_template('page.html', message=message)
<p>{{ message | safe }}</p>
Never use | safe filter or Markup() on user input unless you’ve thoroughly sanitized it.

JavaScript Context

When passing data to JavaScript, use JSON encoding:
<script>
    // Safe: JSON-encoded data
    const userData = {{ user_data | tojson }};
    console.log(userData.name);
</script>
Jinja2’s tojson filter safely encodes Python objects for use in JavaScript context.

Preventing Stored XSS

Stored XSS occurs when malicious input is saved to the database and later displayed to users.

Input Validation + Output Encoding

secure/app.py
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        # Input validation
        username = request.form.get('username', '').strip()
        email = request.form.get('email', '').strip()
        
        # Validate format and length
        if len(username) < 3 or len(username) > 50:
            flash('Invalid username length', 'danger')
            return render_template('register.html')
        
        # Store in database (parameterized query prevents SQL injection)
        query = "INSERT INTO users (username, email) VALUES (%s, %s)"
        cursor.execute(query, (username, email))
        
        # When displaying later, Jinja2 auto-escapes
        return render_template('profile.html', username=username)
1

Validate input

Validate length, format, and type of user input before storing:
if not re.match(r'^[a-zA-Z0-9_]+$', username):
    return error('Invalid username')
2

Store safely

Use parameterized queries to prevent SQL injection:
cursor.execute("INSERT INTO users (username) VALUES (%s)", (username,))
3

Encode output

Let Jinja2 auto-escape or use manual escaping:
<h1>{{ username }}</h1>

Preventing Reflected XSS

Reflected XSS occurs when user input from the URL is displayed without proper encoding:
secure/app.py
@app.route('/dashboard')
def dashboard():
    if 'user_id' not in session:
        return redirect('/login')
    
    # Query parameter from URL
    message = request.args.get('message', '')
    
    # Escape before displaying
    return render_template('dashboard.html',
                         message=escape(message))
Even though Jinja2 auto-escapes, explicitly using escape() in Python code makes your security intentions clear and provides defense in depth.

Content Security Policy (CSP)

CSP headers provide an additional layer of XSS protection by restricting resource loading:
secure/app.py
@app.after_request
def set_security_headers(response):
    response.headers['Content-Security-Policy'] = "default-src 'self'"
    return response
This policy:
  • Only allows resources from the same origin
  • Blocks inline scripts and styles
  • Prevents loading external resources
See the Security Headers page for comprehensive CSP configuration.

Rich Text and HTML Input

If you must accept HTML input (e.g., for a blog or forum), use a sanitization library:
from bleach import clean

# Whitelist allowed tags and attributes
allowed_tags = ['p', 'br', 'strong', 'em', 'a']
allowed_attrs = {'a': ['href', 'title']}

user_html = request.form.get('content')
safe_html = clean(user_html, tags=allowed_tags, attributes=allowed_attrs)

# Store safe_html in database
Libraries like Bleach (Python) or DOMPurify (JavaScript) provide comprehensive HTML sanitization.

Common XSS Vectors to Prevent

Script Tags

<script>alert('XSS')</script>
Prevention: Auto-escaping converts to &lt;script&gt;

Event Handlers

<img src=x onerror="alert('XSS')">
Prevention: Auto-escaping prevents attribute injection

JavaScript URLs

<a href="javascript:alert('XSS')">Click</a>
Prevention: Validate and sanitize href attributes

Data URIs

<img src="data:text/html,<script>alert('XSS')</script>">
Prevention: CSP and input validation

Testing for XSS

Test your application with common XSS payloads:
test_payloads = [
    "<script>alert('XSS')</script>",
    "<img src=x onerror=alert('XSS')>",
    "<svg onload=alert('XSS')>",
    "javascript:alert('XSS')",
    "<iframe src=javascript:alert('XSS')>"
]

for payload in test_payloads:
    response = client.post('/submit', data={'content': payload})
    # Verify payload is properly escaped in response
    assert payload not in response.data.decode()

Best Practices Summary

1

Trust Jinja2 auto-escaping

For template output, rely on Jinja2’s automatic HTML escaping.
2

Manually escape in Python

Use escape() when handling user input in Python code before passing to templates.
3

Use context-specific encoding

Apply appropriate encoding for HTML, JavaScript, URL, and CSS contexts.
4

Never use |safe on user input

Avoid | safe filter unless you’ve sanitized with a library like Bleach.
5

Implement CSP headers

Use Content-Security-Policy headers as defense in depth.
6

Validate input

Combine input validation with output encoding for maximum protection.

Next Steps

Security Headers

Implement comprehensive security headers including CSP

Input Validation

Learn about validating user input before storage

Build docs developers (and LLMs) love