Jinja2 Template Autoescape Disabled Vulnerability

High Risk Cross-Site Scripting (XSS)
Jinja2PythonXSSTemplate InjectionHTML EscapingWeb SecurityFlaskDjango

What it is

Jinja2 templates are configured with autoescape disabled, allowing unescaped user input to be rendered directly into HTML, creating Cross-Site Scripting (XSS) vulnerabilities.

from jinja2 import Environment, FileSystemLoader from flask import Flask, render_template_string, request # Vulnerable: Autoescape disabled env = Environment( loader=FileSystemLoader('templates'), autoescape=False # Dangerous setting ) @app.route('/profile') def profile(): # Vulnerable: User input rendered without escaping username = request.args.get('name', 'Anonymous') template = env.get_template('profile.html') return template.render(username=username) @app.route('/comment') def show_comment(): # Vulnerable: Direct string template with no escaping comment = request.args.get('comment') template_str = f'
Comment: {comment}
' return render_template_string(template_str) # Vulnerable template configuration app.jinja_env.autoescape = False
from jinja2 import Environment, FileSystemLoader, select_autoescape from flask import Flask, render_template_string, request from markupsafe import escape # Secure: Autoescape enabled env = Environment( loader=FileSystemLoader('templates'), autoescape=select_autoescape(['html', 'xml']) # Safe setting ) @app.route('/profile') def profile(): # Secure: Autoescape will handle HTML escaping username = request.args.get('name', 'Anonymous') template = env.get_template('profile.html') return template.render(username=username) @app.route('/comment') def show_comment(): # Secure: Explicitly escape user input comment = request.args.get('comment') safe_comment = escape(comment) template_str = f'
Comment: {safe_comment}
' return render_template_string(template_str) # Secure: Enable autoescape for Flask app.jinja_env.autoescape = True # Alternative: Use safe rendering with validation @app.route('/safe_render') def safe_render(): user_input = request.args.get('input', '') # Validate input before rendering if '

Why it happens

Code creates Jinja2 environment with autoescape=False: Environment(autoescape=False). Default templates don't escape HTML special characters. User input rendered as raw HTML enables XSS. Common in custom template engines or when migrating from non-auto-escaping frameworks. All templates inherit disabled autoescape.

Root causes

Disabling Jinja2 autoescape in Environment Configuration

Code creates Jinja2 environment with autoescape=False: Environment(autoescape=False). Default templates don't escape HTML special characters. User input rendered as raw HTML enables XSS. Common in custom template engines or when migrating from non-auto-escaping frameworks. All templates inherit disabled autoescape.

Using |safe Filter on User-Controlled Template Variables

Templates mark user data as safe: {{ user_input|safe }}. Bypasses auto-escaping. Developers use |safe assuming input sanitized elsewhere. Often validation incomplete or bypassed. Marking untrusted data safe causes XSS even with autoescape enabled. Safe filter should only mark trusted content.

Using Markup() Constructor on Unsanitized User Input

Code wraps user data: from markupsafe import Markup; content = Markup(user_input). Markup objects bypass auto-escaping when rendered. Developers use Markup for convenience, forgetting security implications. Even with HTML sanitization, incomplete blocklists allow XSS. Markup should only wrap trusted strings.

Rendering Templates with render_template_string on User Input

Using user data as template source: render_template_string(request.args['template']). User controls template content, injecting Jinja2 expressions: {{ config }}. Enables server-side template injection (SSTI) accessing Python objects, environment variables, or executing code. Template string source must never be user-controlled.

Setting autoescape=select_autoescape() with Incomplete Extensions

Configuring selective autoescape: Environment(autoescape=select_autoescape(enabled_extensions=['html'])). Missing extensions like .htm, .xml get no escaping. Files without extensions also unprotected. Incomplete configuration creates inconsistent protection. Some templates vulnerable while others protected, depending on file extension.

Fixes

1

Always Enable autoescape When Creating Jinja2 Environment

Set autoescape=True explicitly: Environment(autoescape=True) or env = Environment(autoescape=select_autoescape(default_for_string=True, default=True)). Enables escaping for all templates. Flask's render_template() has autoescape enabled by default. Never disable autoescape unless rendering plaintext.

2

Never Use |safe Filter on User-Controlled Data

Reserve |safe for trusted content only: developer-generated HTML, sanitized markdown output. For user data, rely on automatic escaping: {{ user_input }}. If HTML needed, sanitize with bleach library first: clean_html = bleach.clean(user_html, tags=['p', 'b', 'i']). Then mark safe.

3

Avoid Markup() Constructor, Use Automatic Escaping

Don't wrap user data in Markup(): avoid Markup(user_content). Pass strings normally, let Jinja2 escape: render_template('page.html', content=user_content). For trusted HTML from sanitization libraries, verify sanitizer configuration before using Markup(). Limit Markup() to static developer-controlled content.

4

Never Use render_template_string() with User Input

Avoid render_template_string() entirely for user data. Use pre-defined template files with render_template('template.html', data=user_data). If dynamic templates required, use sandboxed environment: from jinja2.sandbox import SandboxedEnvironment; env = SandboxedEnvironment(). Sandbox restricts attribute access, preventing SSTI.

5

Configure select_autoescape() to Cover All Template Types

Use comprehensive autoescape configuration: autoescape=select_autoescape(enabled_extensions=['html', 'htm', 'xml', 'xhtml'], default_for_string=True, default=True). Include all extensions. Set default=True for unknown extensions. Ensures consistent escaping across all templates regardless of extension or source.

6

Implement Content Security Policy Headers for Defense-in-Depth

Add CSP headers even with autoescape: response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'". Prevents inline scripts. Combine with X-XSS-Protection, X-Content-Type-Options. CSP provides protection layer if escaping bypassed. Use nonce-based CSP for maximum security.

Detect This Vulnerability in Your Code

Sourcery automatically identifies jinja2 template autoescape disabled vulnerability and many other security issues in your codebase.