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 = '''
        <h1>User Activity Report</h1>
        <p>User: {{ user }}</p>
        <p>Period: {{ start_date }} to {{ end_date }}</p>
        <p>Generated: {{ timestamp }}</p>
        '''
    elif report_data['type'] == 'system_status':
        template = '''
        <h1>System Status Report</h1>
        <p>Period: {{ start_date }} to {{ end_date }}</p>
        <p>Generated: {{ timestamp }}</p>
        <p>Status: Operational</p>
        '''
    elif report_data['type'] == 'error_summary':
        template = '''
        <h1>Error Summary Report</h1>
        <p>Period: {{ start_date }} to {{ end_date }}</p>
        <p>Generated: {{ timestamp }}</p>
        <p>Total Errors: 0</p>
        '''
    
    # 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.