Python Mako Templates Security Detection

High Risk Template Injection
PythonMakoTemplate InjectionCode ExecutionTemplate EngineSecurity Audit

What it is

Application uses Mako templates which can execute arbitrary Python code if user input reaches template rendering, creating potential remote code execution vulnerabilities.

from mako.template import Template from flask import Flask, request @app.route('/render_template') def render_template(): # Vulnerable: User input directly in Mako template user_input = request.args.get('content', '') # Dangerous: Mako can execute arbitrary Python code template_str = f'Hello, {user_input}!' template = Template(template_str) return template.render() @app.route('/custom_page') def custom_page(): # Vulnerable: User-controlled template content page_content = request.form.get('content') # Extremely dangerous: Full template control to user template = Template(page_content) return template.render(user='admin', role='user') @app.route('/dynamic_template') def dynamic_template(): # Vulnerable: Code execution via template variables data = request.get_json() template_str = '''<% result = eval(data['expression']) %> Result: ${result}''' template = Template(template_str) return template.render(data=data)
from mako.template import Template from mako.lookup import TemplateLookup from mako import exceptions from flask import Flask, request import re import html # Secure template configuration class SecureTemplateLookup(TemplateLookup): """Secure template lookup with restrictions.""" def __init__(self, *args, **kwargs): # Disable code execution features kwargs['default_filters'] = ['h'] # HTML escape by default kwargs['strict_undefined'] = True super().__init__(*args, **kwargs) # Safe template rendering function def safe_render_template(template_string, variables=None, max_length=1000): """Safely render template with validation.""" if len(template_string) > max_length: raise ValueError('Template too long') # Check for dangerous patterns dangerous_patterns = [ r'<%.*%>', # Code blocks r'eval\(', r'exec\(', r'import\s+', r'__.*__', # Dunder methods r'\${.*\(.*\)}', # Function calls in expressions ] for pattern in dangerous_patterns: if re.search(pattern, template_string, re.IGNORECASE): raise ValueError(f'Dangerous template pattern detected: {pattern}') # Sanitize variables safe_variables = {} if variables: for key, value in variables.items(): if isinstance(key, str) and key.isalnum(): if isinstance(value, (str, int, float, bool)): safe_variables[key] = value try: # Create template with restrictions template = Template( template_string, default_filters=['h'], # HTML escape strict_undefined=True ) return template.render(**safe_variables) except exceptions.MakoException as e: raise ValueError(f'Template rendering failed: {str(e)}') @app.route('/render_template') def render_template(): """Secure template rendering with validation.""" user_input = request.args.get('content', '') # Validate and sanitize input if not user_input: return 'No content provided', 400 # Only allow alphanumeric and basic punctuation if not re.match(r'^[a-zA-Z0-9\s.,!?\-_]+$', user_input): return 'Invalid characters in content', 400 # Limit length if len(user_input) > 100: return 'Content too long', 400 try: # Safe template with escaped content template_str = 'Hello, ${content | h}!' # |h forces HTML escaping result = safe_render_template( template_str, {'content': user_input} ) return result except ValueError as e: return f'Template error: {str(e)}', 400 @app.route('/custom_page') def custom_page(): """Secure custom page with predefined templates.""" # Don't allow user-controlled templates return 'Custom templates disabled for security', 403 # Alternative: Use predefined safe templates SAFE_TEMPLATES = { 'welcome': 'Welcome, ${username | h}!', 'profile': 'User: ${username | h}, Role: ${role | h}', 'message': 'Message: ${text | h}' } @app.route('/safe_template/') def safe_template(template_name): """Render predefined safe templates only.""" if template_name not in SAFE_TEMPLATES: return 'Template not found', 404 # Get template variables from query parameters variables = {} for key in ['username', 'role', 'text']: value = request.args.get(key, '') if value and len(value) <= 50 and value.replace(' ', '').isalnum(): variables[key] = value try: template_str = SAFE_TEMPLATES[template_name] result = safe_render_template(template_str, variables) return result except ValueError as e: return f'Template error: {str(e)}', 400 # Secure alternative: Use Jinja2 instead @app.route('/jinja_alternative') def jinja_alternative(): """Secure alternative using Jinja2.""" from jinja2 import Environment, select_autoescape # Secure Jinja2 environment env = Environment( autoescape=select_autoescape(['html', 'xml']), trim_blocks=True, lstrip_blocks=True ) user_input = request.args.get('content', '') # Validate input if not re.match(r'^[a-zA-Z0-9\s.,!?\-_]+$', user_input): return 'Invalid input', 400 # Safe template rendering with Jinja2 template = env.from_string('Hello, {{ content }}!') return template.render(content=user_input) # Template security checker def check_template_security(template_content): """Check template for security issues.""" issues = [] # Check for code execution patterns if '<%' in template_content and '%>' in template_content: issues.append('Code blocks detected') if re.search(r'eval|exec|import|__.*__', template_content): issues.append('Dangerous function calls detected') if '${' in template_content and '(' in template_content: issues.append('Function calls in expressions detected') return issues @app.route('/validate_template', methods=['POST']) def validate_template(): """Validate template for security issues.""" template_content = request.form.get('template', '') if not template_content: return {'error': 'No template provided'}, 400 issues = check_template_security(template_content) if issues: return { 'valid': False, 'issues': issues }, 400 return { 'valid': True, 'message': 'Template appears safe' }

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Code uses Mako templates: from mako.template import Template; Template(template_str).render(**data). Mako doesn't auto-escape by default unlike Jinja2. HTML special characters rendered as-is. User input in templates causes XSS. Developers expecting auto-escaping get vulnerable applications.

Root causes

Using Mako Templates Without Default Auto-Escaping

Code uses Mako templates: from mako.template import Template; Template(template_str).render(**data). Mako doesn't auto-escape by default unlike Jinja2. HTML special characters rendered as-is. User input in templates causes XSS. Developers expecting auto-escaping get vulnerable applications.

Migrating from Auto-Escaping Frameworks to Mako

Teams switching from Django or Flask to Mako. Previous frameworks had auto-escaping enabled by default. Mako requires explicit escaping: ${data | h}. Migration without understanding escaping differences creates vulnerabilities. Code working securely in Jinja2 becomes vulnerable in Mako.

Using ${} Expression Syntax Without HTML Escaping

Expressions without filters: ${user_input}. Raw value insertion without escaping. HTML characters like <>&"' not escaped. Common pattern for displaying user data. Mako $ expressions output raw content, enabling XSS when used with untrusted data.

Not Configuring default_filters in Template Initialization

Creating templates without default filters: Template(text). Missing default_filters=['h'] for HTML escaping. No application-wide escaping configuration. Each template needs manual escaping. Forgetting |h filter in any expression creates XSS vulnerability. Manual escaping error-prone across large codebases.

Using Mako for HTML Templates Without Security Training

Developers unfamiliar with Mako security model. Using Mako because of performance or syntax preferences. Not reading security documentation. Missing awareness of manual escaping requirement. Template security training focused on auto-escaping frameworks. Mako requires different security approach.

Fixes

1

Always Use |h Filter for HTML Escaping in Expressions

Escape all user data: ${user_input | h}. Filter escapes HTML special characters. Apply to all expressions with untrusted data. Use ${data | u} for URL escaping. Combine filters: ${data | u, h}. Manual escaping required for every expression.

2

Configure default_filters=['h'] for All Templates

Set default escaping in TemplateLookup: from mako.lookup import TemplateLookup; lookup = TemplateLookup(default_filters=['h']). All $ expressions escaped by default. Or in Template: Template(text, default_filters=['h']). Application-wide escaping configuration. Use ${data | n} to bypass escaping when needed.

3

Migrate to Jinja2 for Better Security Defaults

Replace Mako with Jinja2: from jinja2 import Template; Template(template_str).render(data). Jinja2 auto-escapes by default. Better security model for web applications. Similar syntax. Less error-prone. Flask and Django use Jinja2. Migration improves security posture significantly.

4

Use strict_undefined=True to Detect Template Errors

Enable strict mode: Template(text, strict_undefined=True). Raises exceptions for undefined variables instead of rendering empty strings. Catches typos in variable names. Prevents silent failures. Helps identify when escaping filters accidentally omitted. Better error detection during development.

5

Implement Template Security Review and Testing

Code review all templates for escaping: grep '${' templates/*.html. Verify each expression has |h or justified |n. Automated testing with XSS payloads. Template linting tools checking for unescaped expressions. Security training on Mako-specific requirements. Regular audits of template code.

6

Use Context-Specific Escaping for JavaScript and CSS

JavaScript context needs JSON escaping: <script>var data = ${json.dumps(data)};</script>. Not |h which is HTML-only. CSS contexts require different escaping. URL contexts use |u. Understand escaping context requirements. Wrong escaping type provides no protection. Match filter to context.

Detect This Vulnerability in Your Code

Sourcery automatically identifies python mako templates security detection and many other security issues in your codebase.