Flask API Method String Format Vulnerability

High Risk Format String Injection
flaskpythonapiformat-stringinjectionstring-formatting

What it is

The Flask application uses string formatting in API method handlers with user-controlled input without proper validation, creating format string injection vulnerabilities. When user input is incorporated into format strings within API methods, attackers can inject format specifiers to access memory, read sensitive data, cause application crashes, or potentially achieve code execution through format string exploitation in API responses or internal processing.

# Vulnerable: Format string injection in Flask API methods

from flask import Flask, request, jsonify
import datetime
import logging

app = Flask(__name__)

# Dangerous: API method with format string injection
@app.route('/api/users/<user_id>/profile', methods=['GET'])
def get_user_profile(user_id):
    template = request.args.get('template', 'User: %s')
    
    # CRITICAL: User controls format string
    try:
        formatted_response = template % user_id
        return jsonify({'profile': formatted_response})
    except Exception as e:
        return jsonify({'error': str(e)})

# Another dangerous pattern in API
@app.route('/api/search', methods=['POST'])
def search_api():
    query = request.json.get('query', '')
    format_type = request.json.get('format', '%s')
    
    # Dangerous: Format string from user input
    try:
        search_results = perform_search(query)
        formatted_results = []
        
        for result in search_results:
            # User controls format for each result
            formatted_result = format_type % result
            formatted_results.append(formatted_result)
        
        return jsonify({'results': formatted_results})
    except Exception as e:
        return jsonify({'error': str(e)})

# Log formatting in API
@app.route('/api/log', methods=['POST'])
def api_log_entry():
    log_template = request.json.get('template', '')
    log_data = request.json.get('data', {})
    
    # Dangerous: User-controlled log template
    try:
        log_message = log_template % log_data
        app.logger.info(log_message)
        
        return jsonify({'status': 'logged', 'message': log_message})
    except Exception as e:
        return jsonify({'error': str(e)})

# Report generation API
@app.route('/api/reports/generate', methods=['POST'])
def generate_api_report():
    report_template = request.json.get('template', '')
    report_data = request.json.get('data', {})
    
    # Dangerous: Format string in report generation
    try:
        report_content = report_template % report_data
        
        return jsonify({
            'report_id': 'rpt_123',
            'content': report_content,
            'generated_at': datetime.datetime.now().isoformat()
        })
    except Exception as e:
        return jsonify({'error': str(e)})

# Message formatting API
@app.route('/api/messages/format', methods=['POST'])
def format_message_api():
    message_template = request.json.get('template', '')
    variables = request.json.get('variables', {})
    
    # Dangerous: User controls message template
    try:
        formatted_message = message_template % variables
        
        return jsonify({
            'formatted_message': formatted_message,
            'template_used': message_template
        })
    except Exception as e:
        return jsonify({'error': str(e)})

# Error message API
@app.route('/api/errors/format', methods=['GET'])
def format_error_api():
    error_template = request.args.get('template', '')
    error_code = request.args.get('code', '')
    error_details = request.args.get('details', '')
    
    # Dangerous: Format string in error handling
    try:
        error_data = {
            'code': error_code,
            'details': error_details,
            'timestamp': datetime.datetime.now()
        }
        
        formatted_error = error_template % error_data
        
        return jsonify({'error_message': formatted_error})
    except Exception as e:
        return jsonify({'error': str(e)})

# Configuration API with formatting
@app.route('/api/config/display', methods=['POST'])
def display_config_api():
    config_template = request.json.get('template', '')
    config_section = request.json.get('section', 'general')
    
    # Get configuration data
    config_data = get_config_data(config_section)
    
    # Dangerous: User-controlled config display template
    try:
        formatted_config = config_template % config_data
        
        return jsonify({
            'section': config_section,
            'formatted_config': formatted_config
        })
    except Exception as e:
        return jsonify({'error': str(e)})

# Notification API with format strings
@app.route('/api/notifications/send', methods=['POST'])
def send_notification_api():
    notification_template = request.json.get('template', '')
    recipient = request.json.get('recipient', '')
    notification_data = request.json.get('data', {})
    
    # Dangerous: Format string in notification
    try:
        # Add recipient to data
        notification_data['recipient'] = recipient
        
        formatted_notification = notification_template % notification_data
        
        # Send notification (mock)
        send_notification(recipient, formatted_notification)
        
        return jsonify({
            'status': 'sent',
            'recipient': recipient,
            'message': formatted_notification
        })
    except Exception as e:
        return jsonify({'error': str(e)})

def perform_search(query):
    # Mock search function
    return ['result1', 'result2', 'result3']

def get_config_data(section):
    # Mock config function
    return {
        'setting1': 'value1',
        'setting2': 'value2',
        'debug': True
    }

def send_notification(recipient, message):
    # Mock notification function
    pass
# Secure: Safe string formatting in Flask API methods

from flask import Flask, request, jsonify
from marshmallow import Schema, fields, ValidationError as MarshmallowValidationError
import datetime
import logging
import re
from markupsafe import escape

app = Flask(__name__)

# Safe: Input validation schemas
class UserProfileSchema(Schema):
    template_type = fields.Str(validate=lambda x: x in ['simple', 'detailed', 'minimal'])

class SearchSchema(Schema):
    query = fields.Str(required=True, validate=lambda x: len(x) <= 100)
    format_type = fields.Str(validate=lambda x: x in ['json', 'text', 'summary'])

class LogEntrySchema(Schema):
    level = fields.Str(validate=lambda x: x in ['info', 'warning', 'error'])
    message = fields.Str(required=True, validate=lambda x: len(x) <= 500)
    context = fields.Dict(missing={})

# Safe: User profile API with validation
@app.route('/api/users/<user_id>/profile', methods=['GET'])
def safe_get_user_profile(user_id):
    try:
        # Validate user ID
        validated_user_id = validate_user_id(user_id)
        
        # Validate query parameters
        schema = UserProfileSchema()
        args = schema.load(request.args)
        
        # Get profile data
        profile_data = get_user_profile_data(validated_user_id)
        
        # Format response safely
        formatted_response = format_user_profile_safely(profile_data, args.get('template_type', 'simple'))
        
        return jsonify(formatted_response)
    except (ValueError, MarshmallowValidationError) as e:
        return jsonify({'error': str(e)}), 400

def validate_user_id(user_id):
    if not user_id or not user_id.isdigit():
        raise ValueError('Invalid user ID format')
    
    user_id_int = int(user_id)
    if user_id_int <= 0 or user_id_int > 999999:
        raise ValueError('User ID out of range')
    
    return user_id_int

def get_user_profile_data(user_id):
    # Mock function - would query database
    return {
        'id': user_id,
        'username': f'user_{user_id}',
        'email': f'user{user_id}@example.com',
        'created_at': '2023-01-01T00:00:00Z'
    }

def format_user_profile_safely(profile_data, template_type):
    # Safe: Predefined response formats
    if template_type == 'simple':
        return {
            'profile': {
                'id': profile_data['id'],
                'username': profile_data['username']
            }
        }
    elif template_type == 'detailed':
        return {
            'profile': profile_data
        }
    elif template_type == 'minimal':
        return {
            'profile': {
                'id': profile_data['id']
            }
        }

# Safe: Search API with validation
@app.route('/api/search', methods=['POST'])
def safe_search_api():
    try:
        # Validate input
        schema = SearchSchema()
        data = schema.load(request.json or {})
        
        # Perform search
        search_results = perform_safe_search(data['query'])
        
        # Format results safely
        formatted_results = format_search_results_safely(
            search_results, 
            data.get('format_type', 'json')
        )
        
        return jsonify({
            'results': formatted_results,
            'total': len(search_results),
            'query': data['query']
        })
    except MarshmallowValidationError as e:
        return jsonify({'error': e.messages}), 400

def perform_safe_search(query):
    # Safe: Sanitize query
    clean_query = re.sub(r'[^a-zA-Z0-9\s-]', '', query)
    
    # Mock search - would use database/search engine
    mock_results = [
        {'id': 1, 'title': 'Result 1', 'score': 0.95},
        {'id': 2, 'title': 'Result 2', 'score': 0.87},
        {'id': 3, 'title': 'Result 3', 'score': 0.76}
    ]
    
    # Filter based on query (mock)
    return mock_results

def format_search_results_safely(results, format_type):
    # Safe: Structured formatting
    if format_type == 'json':
        return results
    elif format_type == 'text':
        return [f"#{result['id']}: {result['title']}" for result in results]
    elif format_type == 'summary':
        return {
            'count': len(results),
            'top_result': results[0] if results else None
        }

# Safe: Logging API with validation
@app.route('/api/log', methods=['POST'])
def safe_api_log_entry():
    try:
        # Validate input
        schema = LogEntrySchema()
        data = schema.load(request.json or {})
        
        # Create log entry safely
        create_safe_log_entry(data)
        
        return jsonify({'status': 'logged successfully'})
    except MarshmallowValidationError as e:
        return jsonify({'error': e.messages}), 400

def create_safe_log_entry(log_data):
    level = log_data.get('level', 'info')
    message = log_data['message']
    context = log_data.get('context', {})
    
    # Safe: Sanitize message
    clean_message = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', message)
    
    # Safe: Structured logging
    log_entry = {
        'timestamp': datetime.datetime.utcnow().isoformat(),
        'level': level,
        'message': clean_message,
        'context': context,
        'source': 'api'
    }
    
    # Use appropriate logging level
    if level == 'info':
        app.logger.info('API Log: %(message)s', log_entry)
    elif level == 'warning':
        app.logger.warning('API Log: %(message)s', log_entry)
    elif level == 'error':
        app.logger.error('API Log: %(message)s', log_entry)

# Safe: Report generation API
@app.route('/api/reports/generate', methods=['POST'])
def safe_generate_api_report():
    try:
        # Validate input
        report_data = validate_report_request(request.json or {})
        
        # Generate report safely
        report = generate_safe_report(report_data)
        
        return jsonify(report)
    except ValueError as e:
        return jsonify({'error': str(e)}), 400

def validate_report_request(data):
    report_type = data.get('type', '')
    parameters = data.get('parameters', {})
    
    # Validate report type
    allowed_types = ['user_activity', 'system_status', 'error_summary']
    if report_type not in allowed_types:
        raise ValueError('Invalid report type')
    
    # Validate parameters based on type
    if report_type == 'user_activity':
        required_params = ['start_date', 'end_date']
        for param in required_params:
            if param not in parameters:
                raise ValueError(f'Missing parameter: {param}')
        
        # Validate dates
        try:
            datetime.datetime.fromisoformat(parameters['start_date'])
            datetime.datetime.fromisoformat(parameters['end_date'])
        except ValueError:
            raise ValueError('Invalid date format')
    
    return {
        'type': report_type,
        'parameters': parameters
    }

def generate_safe_report(report_data):
    report_type = report_data['type']
    parameters = report_data['parameters']
    
    # Safe: Predefined report templates
    if report_type == 'user_activity':
        return {
            'report_id': f'rpt_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'type': 'user_activity',
            'period': {
                'start': parameters['start_date'],
                'end': parameters['end_date']
            },
            'data': {
                'total_users': 1000,
                'active_users': 250,
                'new_registrations': 15
            },
            'generated_at': datetime.datetime.utcnow().isoformat()
        }
    elif report_type == 'system_status':
        return {
            'report_id': f'rpt_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'type': 'system_status',
            'data': {
                'status': 'operational',
                'uptime': '99.9%',
                'response_time': '150ms'
            },
            'generated_at': datetime.datetime.utcnow().isoformat()
        }
    elif report_type == 'error_summary':
        return {
            'report_id': f'rpt_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}',
            'type': 'error_summary',
            'data': {
                'total_errors': 5,
                'critical_errors': 0,
                'error_rate': '0.01%'
            },
            'generated_at': datetime.datetime.utcnow().isoformat()
        }

# Safe: Message formatting API
@app.route('/api/messages/format', methods=['POST'])
def safe_format_message_api():
    try:
        # Validate input
        message_data = validate_message_request(request.json or {})
        
        # Format message safely
        formatted_message = format_message_safely(message_data)
        
        return jsonify(formatted_message)
    except ValueError as e:
        return jsonify({'error': str(e)}), 400

def validate_message_request(data):
    template_type = data.get('template_type', '')
    variables = data.get('variables', {})
    
    # Validate template type
    allowed_templates = ['greeting', 'notification', 'reminder', 'confirmation']
    if template_type not in allowed_templates:
        raise ValueError('Invalid template type')
    
    # Validate variables
    if not isinstance(variables, dict):
        raise ValueError('Variables must be a dictionary')
    
    # Sanitize variable values
    sanitized_variables = {}
    for key, value in variables.items():
        if isinstance(value, str) and len(value) <= 100:
            sanitized_variables[key] = escape(value)
        elif isinstance(value, (int, float, bool)):
            sanitized_variables[key] = value
    
    return {
        'template_type': template_type,
        'variables': sanitized_variables
    }

def format_message_safely(message_data):
    template_type = message_data['template_type']
    variables = message_data['variables']
    
    # Safe: Predefined message templates
    templates = {
        'greeting': 'Hello {name}, welcome to our service!',
        'notification': 'You have {count} new notifications.',
        'reminder': 'Don\'t forget about {event} on {date}.',
        'confirmation': 'Your {action} has been completed successfully.'
    }
    
    template = templates[template_type]
    
    try:
        # Safe: Use .format() with validated variables
        formatted_message = template.format(**variables)
        
        return {
            'template_type': template_type,
            'formatted_message': formatted_message,
            'variables_used': list(variables.keys())
        }
    except KeyError as e:
        raise ValueError(f'Missing template variable: {e}')

# Safe: Error handling API
@app.route('/api/errors/format', methods=['GET'])
def safe_format_error_api():
    try:
        error_code = request.args.get('code', '')
        
        # Validate error code
        validated_code = validate_error_code(error_code)
        
        # Get error information safely
        error_info = get_safe_error_info(validated_code)
        
        return jsonify(error_info)
    except ValueError as e:
        return jsonify({'error': str(e)}), 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: {
            'code': 400,
            'message': 'Bad Request',
            'description': 'The request was invalid or malformed'
        },
        401: {
            'code': 401,
            'message': 'Unauthorized',
            'description': 'Authentication is required'
        },
        403: {
            'code': 403,
            'message': 'Forbidden',
            'description': 'Access to this resource is denied'
        },
        404: {
            'code': 404,
            'message': 'Not Found',
            'description': 'The requested resource was not found'
        },
        500: {
            'code': 500,
            'message': 'Internal Server Error',
            'description': 'An unexpected error occurred'
        }
    }
    
    return error_messages.get(error_code, {
        'code': error_code,
        'message': 'Unknown Error',
        'description': 'An unknown error occurred'
    })

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

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Flask API endpoints format user input into response strings: return {'message': 'Hello {}'.format(request.json['name'])}. While format strings in Python 3 are safer than % formatting, they enable attribute access. With complex objects, attackers may access __class__, __init__, or other attributes.

Root causes

Using User Input in String Format Operations for API Responses

Flask API endpoints format user input into response strings: return {'message': 'Hello {}'.format(request.json['name'])}. While format strings in Python 3 are safer than % formatting, they enable attribute access. With complex objects, attackers may access __class__, __init__, or other attributes.

Formatting Template Responses with Unvalidated Request Data

Views format strings for rendering without escaping: msg = 'User {} logged in'.format(username); return render_template('message.html', message=msg). If template uses |safe filter or message inserted in JavaScript context, formatting bypasses auto-escaping. HTML characters in formatted strings cause XSS.

Using f-strings or .format() in Dynamic API Method Construction

Code constructs method names or attributes from user input: method = f'get_{request.args["resource"]}_data'; result = getattr(api_handler, method)(). Format operations don't directly cause injection, but facilitate unsafe reflection patterns. Combined with getattr(), enables unauthorized method access or code execution.

Format Strings in Error Messages Exposing Sensitive Data

Exception handlers format detailed errors: except Exception as e: return {'error': 'Failed: {}'.format(e)}. Exception messages may contain sensitive data, SQL queries, file paths, or internal configuration. User-triggered exceptions with controlled content can leak information through formatted error responses.

Using String Formatting in SQL Query Construction

Combining format operations with database queries: query = 'SELECT * FROM {} WHERE id = {}'.format(table, user_id). Even without direct SQL execution, creates injection vectors. Format strings don't provide parameterization. String-formatted queries passed to ORM or raw SQL enable SQL injection vulnerabilities.

Fixes

1

Use jsonify() for All API Responses with Structured Data

Return structured data with jsonify(): return jsonify(message='Hello', name=name, status='success'). Flask serializes data safely. Avoid string formatting in API responses. Structure data as dictionaries and lists. jsonify() handles encoding, content-type headers, and escaping automatically.

2

Validate Input Schema Before Any String Operations

Use marshmallow or pydantic for input validation: schema.load(request.json) validates types, formats, and constraints before processing. Define strict schemas with allowed characters. Reject invalid input immediately. Validation prevents format string issues and other injection vectors at API boundary.

3

Never Use String Formatting for SQL Queries

Always use parameterized queries with SQLAlchemy: db.session.execute(text('SELECT * FROM users WHERE id = :id'), {'id': user_id}). Use ORM methods: User.query.filter_by(id=user_id). Never format strings into SQL. Parameterization separates code from data, preventing all SQL injection.

4

Sanitize Error Messages, Never Expose Raw Exceptions

Catch exceptions and return generic messages: except Exception as e: app.logger.error(f'Error: {e}'); return jsonify(error='Internal server error'), 500. Log detailed errors server-side. Return generic messages to clients. Use werkzeug.exceptions.HTTPException for structured error responses with safe defaults.

5

Use Allowlists for Dynamic Method or Attribute Access

If dynamic access required, validate against allowlist: ALLOWED_METHODS = {'user_data', 'profile_data'}; if method in ALLOWED_METHODS: result = getattr(handler, method)(). Never format user input into method names. Use dictionary mapping: HANDLERS = {'user': get_user_data}; HANDLERS[request.args['type']](). Explicit mapping prevents injection.

6

Implement Content Type and Encoding Validation

Validate API responses: @app.after_request ensure_json_response(). Check Content-Type: application/json. Use response.get_json() to parse structured responses. Prevent mixing formatted strings with JSON. Use response schemas to validate output structure before returning, ensuring consistent safe format.

Detect This Vulnerability in Your Code

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