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: <script>alert('XSS')</script> -->
Jinja2 automatically escapes: <, >, &, ", and ' characters to prevent XSS attacks.
Manual Escaping with MarkupSafe
When handling user input in Python code, use escape() from MarkupSafe:
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' ))
Import escape function
from markupsafe import escape
Escape user input
safe_message = escape(user_input)
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)
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.
@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)
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' )
Store safely
Use parameterized queries to prevent SQL injection: cursor.execute( "INSERT INTO users (username) VALUES ( %s )" , (username,))
Encode output
Let Jinja2 auto-escape or use manual escaping:
Preventing Reflected XSS
Reflected XSS occurs when user input from the URL is displayed without proper encoding:
@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:
@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
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 <script>
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
Trust Jinja2 auto-escaping
For template output, rely on Jinja2’s automatic HTML escaping.
Manually escape in Python
Use escape() when handling user input in Python code before passing to templates.
Use context-specific encoding
Apply appropriate encoding for HTML, JavaScript, URL, and CSS contexts.
Never use |safe on user input
Avoid | safe filter unless you’ve sanitized with a library like Bleach.
Implement CSP headers
Use Content-Security-Policy headers as defense in depth.
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