Flask Path Traversal via open() Function

High Risk Path Traversal
flaskpythonpath-traversaldirectory-traversalfile-accessopen-function

What it is

The Flask application uses the open() function with user-controlled file paths without proper validation, enabling path traversal attacks. Attackers can manipulate file paths to access files outside the intended directory structure using sequences like '../' to traverse up the directory tree and access sensitive system files, configuration data, source code, or other restricted content through the Flask application's file handling mechanisms.

# Vulnerable: open() with user input in Flask from flask import Flask, request, jsonify, send_file import os import json app = Flask(__name__) # Dangerous: Direct file reading with user input @app.route('/read-file') def read_file(): filename = request.args.get('filename', '') # CRITICAL: User controls filename, can use ../ file_path = os.path.join('/app/data', filename) try: with open(file_path, 'r') as f: content = f.read() return jsonify({'content': content}) except FileNotFoundError: return jsonify({'error': 'File not found'}), 404 except Exception as e: return jsonify({'error': str(e)}), 500 # Another dangerous pattern @app.route('/user-files/') def get_user_file(user_id): filename = request.args.get('file', '') # Dangerous: Multiple path components from user input file_path = f'/app/users/{user_id}/{filename}' try: with open(file_path, 'r') as f: data = f.read() return jsonify({'data': data}) except Exception as e: return jsonify({'error': str(e)}), 500 # Configuration file access @app.route('/config') def get_config(): config_file = request.args.get('config', 'app.conf') # Dangerous: User-controlled config path config_path = os.path.join('/etc/myapp', config_file) try: with open(config_path, 'r') as f: config_data = f.read() return jsonify({'config': config_data}) except Exception as e: return jsonify({'error': str(e)}) # Template file reading @app.route('/template') def get_template(): template_name = request.args.get('template', '') template_dir = request.args.get('dir', 'default') # Dangerous: User controls template path template_path = os.path.join('/app/templates', template_dir, template_name) try: with open(template_path, 'r') as f: template_content = f.read() return jsonify({'template': template_content}) except Exception as e: return jsonify({'error': str(e)}) # Log file access @app.route('/logs') def get_logs(): log_file = request.args.get('log', '') date = request.args.get('date', '') # Dangerous: Path traversal in log access if date: log_path = f'/var/log/myapp/{date}/{log_file}' else: log_path = f'/var/log/myapp/{log_file}' try: with open(log_path, 'r') as f: log_content = f.read() return jsonify({'logs': log_content}) except Exception as e: return jsonify({'error': str(e)}) # Data file processing @app.route('/process-data', methods=['POST']) def process_data_file(): data_file = request.json.get('data_file', '') # Dangerous: User-controlled data file path data_path = os.path.join('/app/data', data_file) try: with open(data_path, 'r') as f: data = json.load(f) # Process data processed_data = process_json_data(data) return jsonify(processed_data) except Exception as e: return jsonify({'error': str(e)}) # Document download @app.route('/download/') def download_document(doc_id): doc_path = request.args.get('path', '') # Dangerous: User controls document path full_path = os.path.join('/app/documents', doc_path, f'{doc_id}.pdf') try: # Check if file exists before sending with open(full_path, 'rb') as f: # File exists, send it pass return send_file(full_path, as_attachment=True) except FileNotFoundError: return jsonify({'error': 'Document not found'}), 404 # Backup file reading @app.route('/backup/restore') def read_backup(): backup_name = request.args.get('backup', '') backup_type = request.args.get('type', 'daily') # Dangerous: User controls backup path backup_path = f'/backups/{backup_type}/{backup_name}' try: with open(backup_path, 'r') as f: backup_data = f.read() return jsonify({'backup_data': backup_data}) except Exception as e: return jsonify({'error': str(e)}) # Include file functionality @app.route('/include') def include_file(): include_file = request.args.get('file', '') # Dangerous: User controls include path include_path = os.path.join('/app/includes', include_file) try: with open(include_path, 'r') as f: include_content = f.read() # Execute or process the included content return jsonify({'content': include_content}) except Exception as e: return jsonify({'error': str(e)}) def process_json_data(data): # Mock processing function return {'processed': True, 'count': len(data) if isinstance(data, (list, dict)) else 0}
# Secure: Safe file handling in Flask from flask import Flask, request, jsonify, send_file from pathlib import Path import os import json import re from marshmallow import Schema, fields, ValidationError as MarshmallowValidationError app = Flask(__name__) # Safe: Configuration with allowed paths app.config['SAFE_DATA_DIR'] = '/app/data' app.config['SAFE_CONFIG_DIR'] = '/app/config' app.config['SAFE_TEMPLATE_DIR'] = '/app/templates' app.config['SAFE_LOG_DIR'] = '/app/logs' # Input validation schemas class FileRequestSchema(Schema): filename = fields.Str(required=True, validate=lambda x: len(x) <= 100) class UserFileSchema(Schema): file = fields.Str(required=True, validate=lambda x: len(x) <= 50) # Safe: File reading with validation @app.route('/read-file') def safe_read_file(): filename = request.args.get('filename', '') try: # Validate filename validated_filename = validate_filename(filename) # Get safe file path safe_path = get_safe_file_path(validated_filename, app.config['SAFE_DATA_DIR']) # Read file securely content = read_file_safely(safe_path) return jsonify({'content': content}) except ValueError as e: return jsonify({'error': str(e)}), 400 except FileNotFoundError: return jsonify({'error': 'File not found'}), 404 def validate_filename(filename): if not filename: raise ValueError('Filename is required') if len(filename) > 100: raise ValueError('Filename too long') # Only allow safe characters if not re.match(r'^[a-zA-Z0-9._-]+$', filename): raise ValueError('Filename contains invalid characters') # Prevent hidden files and traversal if filename.startswith('.') or '..' in filename: raise ValueError('Invalid filename format') # Check file extension allowed_extensions = ['.txt', '.json', '.csv', '.md'] if not any(filename.lower().endswith(ext) for ext in allowed_extensions): raise ValueError('File extension not allowed') return filename def get_safe_file_path(filename, base_dir): # Use pathlib for safe path handling base_path = Path(base_dir) file_path = base_path / filename # Resolve path and validate it's within base directory try: resolved_path = file_path.resolve() base_path_resolved = base_path.resolve() # Ensure file is within the base directory resolved_path.relative_to(base_path_resolved) return resolved_path except ValueError: raise ValueError('File path outside allowed directory') def read_file_safely(file_path): try: if not file_path.exists(): raise FileNotFoundError('File not found') # Check file size max_size = 1024 * 1024 # 1MB if file_path.stat().st_size > max_size: raise ValueError('File too large') # Read file with open(file_path, 'r', encoding='utf-8') as f: return f.read() except UnicodeDecodeError: raise ValueError('File encoding error') except PermissionError: raise ValueError('Access denied') except Exception as e: raise ValueError(f'File read error: {str(e)}') # Safe: User file access with validation @app.route('/user-files/') def safe_get_user_file(user_id): try: # Validate user ID validated_user_id = validate_user_id(user_id) # Validate file parameter schema = UserFileSchema() data = schema.load(request.args) filename = data['file'] # Check user access if not check_user_access(validated_user_id, request): return jsonify({'error': 'Access denied'}), 403 # Get safe user file path user_file_path = get_safe_user_file_path(validated_user_id, filename) # Read file content = read_file_safely(user_file_path) return jsonify({'data': content}) except (ValueError, MarshmallowValidationError) as e: return jsonify({'error': str(e)}), 400 except FileNotFoundError: return jsonify({'error': 'File not found'}), 404 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 check_user_access(user_id, request): # Mock access check - would typically check authentication/authorization # For now, just check if user is accessing their own files current_user_id = getattr(request, 'user_id', None) return current_user_id == user_id or getattr(request, 'is_admin', False) def get_safe_user_file_path(user_id, filename): # Validate filename again validated_filename = validate_filename(filename) # Define safe user directory user_dir = Path(app.config['SAFE_DATA_DIR']) / 'users' / str(user_id) return get_safe_file_path(validated_filename, str(user_dir)) # Safe: Configuration access with allowlist @app.route('/config') def safe_get_config(): config_name = request.args.get('config', '') try: # Validate config name with allowlist validated_config = validate_config_name(config_name) # Get config data safely config_data = get_safe_config_data(validated_config) return jsonify({'config': config_data}) except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_config_name(config_name): # Use allowlist for config files allowed_configs = ['app', 'database', 'cache', 'logging'] if not config_name: raise ValueError('Config name is required') if config_name not in allowed_configs: raise ValueError('Configuration not allowed') return config_name def get_safe_config_data(config_name): # Construct safe config path config_path = Path(app.config['SAFE_CONFIG_DIR']) / f'{config_name}.json' try: # Ensure path is within config directory config_path.resolve().relative_to(Path(app.config['SAFE_CONFIG_DIR']).resolve()) if not config_path.exists(): raise ValueError('Configuration file not found') with open(config_path, 'r', encoding='utf-8') as f: return json.load(f) except (ValueError, json.JSONDecodeError, PermissionError) as e: raise ValueError(f'Config read error: {str(e)}') # Safe: Template access with validation @app.route('/template') def safe_get_template(): template_name = request.args.get('template', '') template_category = request.args.get('category', 'default') try: # Validate inputs validated_template = validate_template_request(template_name, template_category) # Get template content safely template_content = get_safe_template_content(validated_template) return jsonify({'template': template_content}) except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_template_request(template_name, category): # Validate template name if not template_name or not re.match(r'^[a-zA-Z0-9_-]+$', template_name): raise ValueError('Invalid template name') # Validate category allowed_categories = ['email', 'web', 'report', 'notification'] if category not in allowed_categories: raise ValueError('Template category not allowed') # Use allowlist for template names per category allowed_templates = { 'email': ['welcome', 'password_reset', 'notification'], 'web': ['header', 'footer', 'sidebar'], 'report': ['summary', 'detailed', 'chart'], 'notification': ['alert', 'reminder', 'update'] } if template_name not in allowed_templates.get(category, []): raise ValueError('Template not allowed in this category') return {'name': template_name, 'category': category} def get_safe_template_content(template_data): template_name = template_data['name'] category = template_data['category'] # Construct safe template path template_dir = Path(app.config['SAFE_TEMPLATE_DIR']) / category template_path = template_dir / f'{template_name}.html' try: # Validate path is within template directory template_path.resolve().relative_to(Path(app.config['SAFE_TEMPLATE_DIR']).resolve()) if not template_path.exists(): raise ValueError('Template not found') return read_file_safely(template_path) except ValueError as e: raise ValueError(f'Template read error: {str(e)}') # Safe: Log access with restrictions @app.route('/logs') def safe_get_logs(): log_name = request.args.get('log', '') try: # Check access permissions if not check_log_access(request): return jsonify({'error': 'Access denied'}), 403 # Validate log name validated_log = validate_log_name(log_name) # Get log content safely log_content = get_safe_log_content(validated_log) return jsonify({'logs': log_content}) except ValueError as e: return jsonify({'error': str(e)}), 400 def check_log_access(request): # Only allow staff/admin access to logs return getattr(request, 'is_staff', False) def validate_log_name(log_name): # Only allow specific log files allowed_logs = ['application', 'error', 'access', 'security'] if log_name not in allowed_logs: raise ValueError('Log file not allowed') return log_name def get_safe_log_content(log_name): # Construct safe log path log_path = Path(app.config['SAFE_LOG_DIR']) / f'{log_name}.log' try: # Validate path is within log directory log_path.resolve().relative_to(Path(app.config['SAFE_LOG_DIR']).resolve()) if not log_path.exists(): raise ValueError('Log file not found') # Read log with size limit max_size = 1024 * 1024 # 1MB if log_path.stat().st_size > max_size: # Read last 1MB with open(log_path, 'rb') as f: f.seek(-max_size, 2) content = f.read().decode('utf-8', errors='ignore') return '... (truncated)\n' + content return read_file_safely(log_path) except ValueError as e: raise ValueError(f'Log read error: {str(e)}') # Safe: Data processing with validation @app.route('/process-data', methods=['POST']) def safe_process_data_file(): try: data_file = request.json.get('data_file', '') if request.json else '' # Validate data file validated_file = validate_data_file(data_file) # Process data safely result = process_data_file_safely(validated_file) return jsonify(result) except ValueError as e: return jsonify({'error': str(e)}), 400 def validate_data_file(data_file): if not data_file: raise ValueError('Data file name is required') # Only allow JSON data files if not data_file.endswith('.json'): raise ValueError('Only JSON data files are allowed') # Validate filename format filename_part = data_file[:-5] # Remove .json if not re.match(r'^[a-zA-Z0-9_-]+$', filename_part): raise ValueError('Invalid data file name') return data_file def process_data_file_safely(data_file): # Get safe data file path data_path = get_safe_file_path(data_file, app.config['SAFE_DATA_DIR']) try: with open(data_path, 'r', encoding='utf-8') as f: data = json.load(f) # Process data safely processed_data = process_json_data_safely(data) return processed_data except json.JSONDecodeError: raise ValueError('Invalid JSON format') except Exception as e: raise ValueError(f'Data processing error: {str(e)}') def process_json_data_safely(data): # Safe data processing if isinstance(data, dict): return { 'processed': True, 'type': 'object', 'key_count': len(data.keys()) } elif isinstance(data, list): return { 'processed': True, 'type': 'array', 'item_count': len(data) } else: return { 'processed': True, 'type': type(data).__name__, 'value': str(data)[:100] # Limit output } if __name__ == '__main__': app.run(debug=False)

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Flask views open files with request data: filename = request.args['file']; open(filename). Attackers inject ../ sequences traversing directory tree: file=../../../../etc/passwd. open() resolves relative paths from current working directory, enabling access to any readable file outside intended directories.

Root causes

Using User Input Directly in open() File Paths

Flask views open files with request data: filename = request.args['file']; open(filename). Attackers inject ../ sequences traversing directory tree: file=../../../../etc/passwd. open() resolves relative paths from current working directory, enabling access to any readable file outside intended directories.

Concatenating User Input with Base Paths Without Validation

Code joins base directory with user input: filepath = '/var/uploads/' + request.form['filename']; open(filepath). String concatenation doesn't normalize paths. Input like ../../../etc/passwd bypasses base directory. Even os.path.join() vulnerable without validation: os.path.join('/uploads', '../config.py') accesses parent directories.

Not Using Path Normalization or Canonicalization

Paths opened without resolving: open(user_path). Relative paths, symbolic links, ., .., and URL-encoded sequences not normalized. Path.resolve() not used. Attackers exploit traversal sequences, symlinks to sensitive files, or encoded traversals %2e%2e%2f. Unnormalized paths enable directory escapes through various techniques.

Insufficient Validation Allowing Encoded or Alternative Traversals

Weak validation checks: if '..' not in filename. Misses URL-encoded ..%2f, Unicode variations, backslashes on Windows \..\, or absolute paths /etc/passwd. Validation after decoding may be inconsistent. Multiple encoding layers, normalization differences, or case variations bypass simple string matching validations.

Using open() with User-Controlled Paths for Arbitrary File Types

Applications allow opening any file type based on user input: mimetype = request.args['type']; open(f'/files/{filename}.{mimetype}'). Attackers control extensions or paths accessing .env files, source code, configuration files, SSH keys, or database credentials. No file type restrictions enable comprehensive system file access.

Fixes

1

Validate Filenames Against Allowlist of Permitted Characters

Restrict filename characters: if not re.match(r'^[a-zA-Z0-9_.-]+$', filename): abort(400). Reject paths with slashes, .., or special characters. Validate filename length. Use secure_filename() from werkzeug.utils: filename = secure_filename(request.files['file'].filename). Removes path components and dangerous characters.

2

Use Path.resolve() and Verify Result Within Base Directory

Resolve and validate paths: from pathlib import Path; base = Path('/uploads').resolve(); filepath = (base / filename).resolve(); if not filepath.is_relative_to(base): abort(400). resolve() canonicalizes path, following symlinks. is_relative_to() ensures result within allowed directory. Prevents all traversal attacks.

3

Use Indirect References with Mapping to Actual File Paths

Map user input to safe file identifiers: file_id = request.args['id']; filename = FILE_MAPPING.get(file_id). Store mappings in database: SELECT filepath FROM files WHERE id = :id AND user_id = :user. Users reference files by ID, not path. Application controls all file access through mapping.

4

Never Allow User Input to Specify Directory Components

Accept only filename, not paths: filename = os.path.basename(request.args['file']). Construct full path in application: filepath = os.path.join(UPLOAD_DIR, filename). Use Path.name to extract filename: Path(user_input).name. Prevents directory traversal by removing all path components from user input.

5

Implement File Type Validation with Content Inspection

Validate file types by content, not extension: import magic; mime = magic.from_buffer(content, mime=True); if mime not in ALLOWED_TYPES: reject(). Check magic bytes. Use imghdr, python-magic, or filetype libraries. Don't trust user-supplied extensions or MIME types. Content-based validation prevents accessing unexpected file types.

6

Use Dedicated File Storage Services Instead of Direct Filesystem

Store files in object storage: boto3 for S3, google-cloud-storage for GCS. Use abstraction libraries: Flask-Uploads, Flask-Storage. Services handle path safety, access control, and isolation. Generate signed URLs for access. Eliminates path traversal by removing direct filesystem operations from application code.

Detect This Vulnerability in Your Code

Sourcery automatically identifies flask path traversal via open() function and many other security issues in your codebase.