{"title":"Path Traversal via os.path.join() on Untrusted Filenames","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[6,7],"flow_lines":[6,9,7,9],"sink_lines":[9],"vulnerable_code":"import os\nfrom flask import request, send_file\n\n@app.route('/api/v1/iot/device/logs')\ndef fetch_device_logs():\n    device_id = request.args.get('device_id')\n    log_filename = request.args.get('log_file', 'system.log')\n    base_dir = '/var/iot/devices'\n    log_path = os.path.join(base_dir, device_id, 'logs', log_filename)\n    if os.path.exists(log_path):\n        return send_file(log_path, as_attachment=True)\n    return {'error': 'Log file not found'}, 404","explanation":"The application accepts untrusted user input from 'device_id' and 'log_file' query parameters and directly passes them to os.path.join() without validation or sanitization. An attacker can inject path traversal sequences (../) in either parameter to escape the intended base directory and access arbitrary files on the filesystem, which are then served via send_file().","remediation":"The fix applies a defense-in-depth approach: first, it validates both user inputs (device_id and log_filename) against strict allowlist regex patterns that reject path separators and traversal characters. Second, it resolves the constructed path to its canonical absolute form using os.path.realpath() and verifies the resolved path starts with the expected base directory prefix, preventing any bypass via symlinks or encoded traversal sequences.","secure_code":"import os\nimport re\nfrom flask import request, send_file\n\n@app.route('/api/v1/iot/device/logs')\ndef fetch_device_logs():\n    device_id = request.args.get('device_id')\n    log_filename = request.args.get('log_file', 'system.log')\n    base_dir = '/var/iot/devices'\n\n    # Validate device_id: only allow alphanumeric characters, hyphens, and underscores\n    if not device_id or not re.match(r'^[a-zA-Z0-9_\\-]+$', device_id):\n        return {'error': 'Invalid device ID'}, 400\n\n    # Validate log_filename: only allow alphanumeric characters, hyphens, underscores, and dots\n    # Also reject any path separators or traversal sequences\n    if not re.match(r'^[a-zA-Z0-9_\\-\\.]+$', log_filename):\n        return {'error': 'Invalid log filename'}, 400\n\n    # Construct the path\n    log_path = os.path.join(base_dir, device_id, 'logs', log_filename)\n\n    # Resolve to absolute path and verify it stays within the intended base directory\n    resolved_path = os.path.realpath(log_path)\n    expected_prefix = os.path.realpath(base_dir) + os.sep\n\n    if not resolved_path.startswith(expected_prefix):\n        return {'error': 'Access denied'}, 403\n\n    if os.path.exists(resolved_path) and os.path.isfile(resolved_path):\n        return send_file(resolved_path, as_attachment=True)\n    return {'error': 'Log file not found'}, 404"}