Flask Format String Direct Return Vulnerability

High Risk Format String Injection
flaskpythonformat-stringinjectionmemory-disclosureuser-input

What it is

The Flask application directly returns format strings containing user input without proper escaping or validation, creating format string injection vulnerabilities. When user-controlled data is incorporated into format strings that are then returned in HTTP responses, attackers can inject format specifiers to access memory, read sensitive data, cause application crashes, or potentially achieve code execution through format string exploitation.

# Vulnerable: Direct format string return in Flask from flask import Flask, request, jsonify import datetime app = Flask(__name__) # Dangerous: Direct format string with user input @app.route('/greet') def greet_user(): name = request.args.get('name', '') message_template = request.args.get('template', 'Hello %s!') # CRITICAL: User controls format string formatted_message = message_template % name return formatted_message # Another dangerous pattern @app.route('/format_data') def format_data(): data = request.args.get('data', '') format_str = request.args.get('format', '%s') # Dangerous: User-controlled format string try: result = format_str % data return result except Exception as e: return f"Format error: {e}" # Log message formatting @app.route('/log') def log_message(): log_template = request.args.get('template', '') log_data = request.args.get('data', '') # Dangerous: Format string in logging try: log_entry = log_template % log_data app.logger.info(log_entry) return f"Logged: {log_entry}" except Exception as e: return f"Logging error: {e}" # Report generation with format strings @app.route('/report') def generate_report(): report_template = request.form.get('template', '') report_data = { 'user': request.form.get('user', ''), 'date': datetime.datetime.now(), 'data': request.form.get('data', '') } # Dangerous: User-controlled template try: report = report_template % report_data return report except Exception as e: return f"Report error: {e}" # Error message formatting @app.route('/error') def show_error(): error_template = request.args.get('error_template', '') error_details = request.args.get('details', '') # Dangerous: Format string in error messages error_message = error_template % error_details return jsonify({'error': error_message}) # Dynamic string building @app.route('/build_string') def build_string(): pattern = request.args.get('pattern', '') values = request.args.getlist('values') # Dangerous: Pattern from user input try: result = pattern % tuple(values) return result except Exception as e: return f"String building error: {e}" # Configuration display @app.route('/config') def show_config(): config_template = request.args.get('template', '') config_data = { 'app_name': 'MyApp', 'version': '1.0', 'debug': app.debug } # Dangerous: User-controlled config template try: config_display = config_template % config_data return config_display except Exception as e: return f"Config error: {e}" # API response formatting @app.route('/api/format') def format_api_response(): response_format = request.json.get('format', '') response_data = request.json.get('data', {}) # Dangerous: Format string in API response try: formatted_response = response_format % response_data return jsonify({'formatted': formatted_response}) except Exception as e: return jsonify({'error': str(e)})
# Secure: Safe string formatting in Flask from flask import Flask, request, jsonify, render_template_string import datetime import re from markupsafe import escape app = Flask(__name__) # Safe: Input validation and template system @app.route('/greet') def safe_greet_user(): name = request.args.get('name', '') template_type = request.args.get('template', 'default') try: # Validate inputs validated_name = validate_user_name(name) validated_template = validate_template_type(template_type) # Safe: Use predefined templates message = generate_safe_greeting(validated_name, validated_template) return message except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_user_name(name): if not name: raise ValueError('Name is required') if len(name) > 50: raise ValueError('Name too long') # Only allow safe characters if not re.match(r'^[a-zA-Z\s.-]+$', name): raise ValueError('Name contains invalid characters') return escape(name) def validate_template_type(template_type): allowed_templates = ['default', 'formal', 'casual', 'business'] if template_type not in allowed_templates: raise ValueError('Invalid template type') return template_type def generate_safe_greeting(name, template_type): # Safe: Predefined templates templates = { 'default': 'Hello, {name}!', 'formal': 'Good day, {name}.', 'casual': 'Hey {name}!', 'business': 'Dear {name},' } template = templates[template_type] # Safe: Use .format() with validated input return template.format(name=name) # Safe: Data formatting with validation @app.route('/format_data') def safe_format_data(): data = request.args.get('data', '') format_type = request.args.get('format', 'string') try: # Validate inputs validated_data = validate_format_data(data) validated_format = validate_format_type(format_type) # Format data safely result = format_data_safely(validated_data, validated_format) return jsonify({'result': result}) except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_format_data(data): if len(data) > 1000: raise ValueError('Data too long') return escape(data) def validate_format_type(format_type): allowed_formats = ['string', 'upper', 'lower', 'title', 'truncate'] if format_type not in allowed_formats: raise ValueError('Invalid format type') return format_type def format_data_safely(data, format_type): # Safe: Predefined formatting operations formatters = { 'string': lambda x: str(x), 'upper': lambda x: str(x).upper(), 'lower': lambda x: str(x).lower(), 'title': lambda x: str(x).title(), 'truncate': lambda x: str(x)[:100] + '...' if len(str(x)) > 100 else str(x) } return formatters[format_type](data) # Safe: Structured logging @app.route('/log') def safe_log_message(): log_level = request.args.get('level', 'info') message = request.args.get('message', '') context = request.args.get('context', '') try: # Validate inputs validated_log = validate_log_request(log_level, message, context) # Log safely log_message_safely(validated_log) return jsonify({'status': 'Message logged successfully'}) except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_log_request(level, message, context): # Validate log level allowed_levels = ['debug', 'info', 'warning', 'error'] if level not in allowed_levels: raise ValueError('Invalid log level') # Validate message if not message or len(message) > 500: raise ValueError('Invalid message length') # Sanitize inputs clean_message = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', message) clean_context = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', context) if context else '' return { 'level': level, 'message': clean_message, 'context': clean_context } def log_message_safely(log_data): # Safe: Structured logging with validated data log_entry = { 'timestamp': datetime.datetime.utcnow().isoformat(), 'level': log_data['level'], 'message': log_data['message'], 'context': log_data['context'] } # Use appropriate logging level if log_data['level'] == 'debug': app.logger.debug('User log: %(message)s [%(context)s]', log_entry) elif log_data['level'] == 'info': app.logger.info('User log: %(message)s [%(context)s]', log_entry) elif log_data['level'] == 'warning': app.logger.warning('User log: %(message)s [%(context)s]', log_entry) elif log_data['level'] == 'error': app.logger.error('User log: %(message)s [%(context)s]', log_entry) # Safe: Report generation with templates @app.route('/report') def safe_generate_report(): report_type = request.form.get('type', '') report_data = { 'user': request.form.get('user', ''), 'start_date': request.form.get('start_date', ''), 'end_date': request.form.get('end_date', '') } try: # Validate inputs validated_report = validate_report_request(report_type, report_data) # Generate report safely report = generate_report_safely(validated_report) return report except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_report_request(report_type, data): # Validate report type allowed_types = ['user_activity', 'system_status', 'error_summary'] if report_type not in allowed_types: raise ValueError('Invalid report type') # Validate user user = data.get('user', '') if user and not re.match(r'^[a-zA-Z0-9_.-]+$', user): raise ValueError('Invalid user format') # Validate dates start_date = data.get('start_date', '') end_date = data.get('end_date', '') if start_date: try: datetime.datetime.strptime(start_date, '%Y-%m-%d') except ValueError: raise ValueError('Invalid start date format') if end_date: try: datetime.datetime.strptime(end_date, '%Y-%m-%d') except ValueError: raise ValueError('Invalid end date format') return { 'type': report_type, 'user': escape(user) if user else '', 'start_date': start_date, 'end_date': end_date } def generate_report_safely(report_data): # Safe: Use Flask templates if report_data['type'] == 'user_activity': template = '''

User Activity Report

User: {{ user }}

Period: {{ start_date }} to {{ end_date }}

Generated: {{ timestamp }}

''' elif report_data['type'] == 'system_status': template = '''

System Status Report

Period: {{ start_date }} to {{ end_date }}

Generated: {{ timestamp }}

Status: Operational

''' elif report_data['type'] == 'error_summary': template = '''

Error Summary Report

Period: {{ start_date }} to {{ end_date }}

Generated: {{ timestamp }}

Total Errors: 0

''' # Safe: Use render_template_string with validated data return render_template_string(template, user=report_data['user'], start_date=report_data['start_date'], end_date=report_data['end_date'], timestamp=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') ) # Safe: Error handling without format strings @app.route('/error') def safe_show_error(): error_code = request.args.get('code', '') try: # Validate error code validated_code = validate_error_code(error_code) # Get safe error message error_info = get_safe_error_info(validated_code) return jsonify(error_info) except ValueError as e: return jsonify({'error': 'Invalid error code'}), 400 def validate_error_code(code): if not code or not code.isdigit(): raise ValueError('Invalid error code format') code_int = int(code) if code_int < 100 or code_int > 999: raise ValueError('Error code out of range') return code_int def get_safe_error_info(error_code): # Safe: Predefined error messages error_messages = { 400: {'message': 'Bad Request', 'description': 'The request was invalid'}, 401: {'message': 'Unauthorized', 'description': 'Authentication required'}, 403: {'message': 'Forbidden', 'description': 'Access denied'}, 404: {'message': 'Not Found', 'description': 'Resource not found'}, 500: {'message': 'Internal Server Error', 'description': 'Server error occurred'} } return error_messages.get(error_code, { 'message': 'Unknown Error', 'description': 'An unknown error occurred' }) # Safe: API response with structured data @app.route('/api/format') def safe_format_api_response(): response_type = request.json.get('type', '') if request.json else '' data = request.json.get('data', {}) if request.json else {} try: # Validate inputs validated_request = validate_api_format_request(response_type, data) # Format response safely response = format_api_response_safely(validated_request) return jsonify(response) except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_api_format_request(response_type, data): # Validate response type allowed_types = ['summary', 'detailed', 'minimal'] if response_type not in allowed_types: raise ValueError('Invalid response type') # Validate data structure if not isinstance(data, dict): raise ValueError('Data must be a dictionary') # Sanitize data values sanitized_data = {} for key, value in data.items(): if isinstance(value, str) and len(value) <= 100: sanitized_data[key] = escape(value) elif isinstance(value, (int, float, bool)): sanitized_data[key] = value return { 'type': response_type, 'data': sanitized_data } def format_api_response_safely(request_data): response_type = request_data['type'] data = request_data['data'] # Safe: Structured response based on type if response_type == 'summary': return { 'type': 'summary', 'count': len(data), 'keys': list(data.keys()) } elif response_type == 'detailed': return { 'type': 'detailed', 'data': data, 'metadata': { 'timestamp': datetime.datetime.utcnow().isoformat(), 'version': '1.0' } } elif response_type == 'minimal': return { 'type': 'minimal', 'status': 'ok' } if __name__ == '__main__': app.run(debug=False)

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Flask views pass request data directly to format operations: msg = 'Hello {}'.format(request.args['name']). While Python 3 format strings aren't directly exploitable like old % formatting, they enable information disclosure. Template format strings with user input can access object attributes: '{user.__class__}'.format(user=current_user).

Root causes

Using User Input Directly in Format Strings

Flask views pass request data directly to format operations: msg = 'Hello {}'.format(request.args['name']). While Python 3 format strings aren't directly exploitable like old % formatting, they enable information disclosure. Template format strings with user input can access object attributes: '{user.__class__}'.format(user=current_user).

Returning Formatted Strings Without Template Escaping

Views format user input and return directly: return 'Welcome {}'.format(name). Without template rendering, HTML special characters aren't escaped. User input like '<script>alert(1)</script>' gets injected into response. Combining string formatting with direct return bypasses Flask's template auto-escaping.

Using Old-Style % String Formatting with Request Data

Legacy code uses % formatting: msg = 'User: %s' % request.form['user']. While less dangerous in Python 3, it still risks injection. Format specifiers can cause crashes or unexpected behavior. Mixing with tuple unpacking creates vulnerabilities when user controls tuple contents.

Formatting Log Messages with Unsanitized User Input

Logging code formats user-controlled data: logger.info('Login: {}'.format(username)). Attackers inject newlines or ANSI codes to forge log entries, hide malicious activity, or exploit log aggregation systems. Log injection enables bypassing monitoring, frame innocent users, or inject malicious data.

Template String Usage Outside of Flask Template Engine

Developers use Python template strings independently: Template('Hello $name').substitute(name=request.args['name']). Template strings can access object attributes and methods. When combined with complex objects from ORM or sessions, attackers may access sensitive attributes or trigger unintended method calls.

Fixes

1

Always Use Flask Templates with Auto-Escaping

Use render_template() instead of string formatting: return render_template('welcome.html', name=name). Flask's Jinja2 auto-escaping handles HTML special characters. Create templates for all user-facing content. Use {{ name }} syntax which escapes by default, preventing injection attacks through template rendering.

2

Use html.escape() When Templates Cannot Be Used

If direct string return necessary, escape user input: from html import escape; return f'Welcome {escape(name)}'. Escapes <>&"' characters. For JSON responses, use jsonify() which handles encoding: return jsonify(message=f'Welcome {name}'). Never return raw formatted strings with user data.

3

Validate and Sanitize Input Before Any String Operations

Implement input validation before formatting: if re.match(r'^[a-zA-Z0-9_]+$', username): ... Limit character sets, length, and patterns. Reject rather than sanitize unexpected input. Use schema validation libraries like marshmallow. Validate at endpoint entry point before any processing.

4

Use Parameterized Logging, Never Format User Input

Pass parameters separately to logger: logger.info('Login: %s', username) instead of logger.info(f'Login: {username}'). Logger handles formatting safely. Prevents log injection and format string issues. Use structured logging with JSON output for better parsing and safety.

5

Avoid Format Strings Entirely for User-Controlled Data

Use concatenation with escaped values or template engines exclusively. Never use .format(), f-strings, % formatting, or Template() with user input. Prefer API responses using jsonify() over formatted strings. Structure data separately from presentation logic. Let framework handle serialization.

6

Implement Content Security Policy Headers

Even with escaping, add defense-in-depth with CSP headers: response.headers['Content-Security-Policy'] = "default-src 'self'". Prevents injected scripts from executing. Combine with X-Content-Type-Options: nosniff and X-Frame-Options: DENY. Use Flask-Talisman extension for comprehensive security headers.

Detect This Vulnerability in Your Code

Sourcery automatically identifies flask format string direct return vulnerability and many other security issues in your codebase.