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>