{"title":"Arbitrary File Read via Unvalidated User-Supplied File Path","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[3,4],"flow_lines":[3,5,6],"sink_lines":[6],"vulnerable_code":"@app.route('/api/v2/iot/device/logs', methods=['GET'])\ndef fetch_device_logs():\n    device_id = request.args.get('device_id')\n    log_type = request.args.get('log_type', 'system')\n    log_path = f\"/var/iot/devices/{device_id}/logs/{log_type}.log\"\n    try:\n        with open(log_path, 'r') as log_file:\n            log_content = log_file.read()\n        return jsonify({\"device\": device_id, \"logs\": log_content, \"status\": \"success\"}), 200\n    except FileNotFoundError:\n        return jsonify({\"error\": \"Log file not found\"}), 404\n    except Exception as e:\n        return jsonify({\"error\": \"Unable to retrieve logs\"}), 500","explanation":"The code constructs a file path using unsanitized user input from request parameters (device_id and log_type) directly in an f-string without validation. An attacker can inject path traversal sequences (../) to escape the intended directory and read arbitrary files on the system.","remediation":"The fix applies a defense-in-depth approach: first, it validates that both device_id and log_type contain only safe alphanumeric characters (plus hyphens and underscores), rejecting any path traversal characters. Second, it resolves the constructed path to its canonical absolute form using os.path.realpath() and verifies the resolved path is still within the intended base directory, preventing any bypass through symlinks or encoded traversal sequences.","secure_code":"@app.route('/api/v2/iot/device/logs', methods=['GET'])\ndef fetch_device_logs():\n    device_id = request.args.get('device_id')\n    log_type = request.args.get('log_type', 'system')\n\n    # Validate device_id and log_type contain only safe characters (alphanumeric, hyphens, underscores)\n    if not device_id or not re.match(r'^[a-zA-Z0-9_\\-]+$', device_id):\n        return jsonify({\"error\": \"Invalid device_id parameter\"}), 400\n    if not re.match(r'^[a-zA-Z0-9_\\-]+$', log_type):\n        return jsonify({\"error\": \"Invalid log_type parameter\"}), 400\n\n    base_dir = '/var/iot/devices'\n    log_path = os.path.join(base_dir, device_id, 'logs', f\"{log_type}.log\")\n\n    # Resolve the absolute path and ensure it stays within the base directory\n    resolved_path = os.path.realpath(log_path)\n    if not resolved_path.startswith(os.path.realpath(base_dir) + os.sep):\n        return jsonify({\"error\": \"Access denied\"}), 403\n\n    try:\n        with open(resolved_path, 'r') as log_file:\n            log_content = log_file.read()\n        return jsonify({\"device\": device_id, \"logs\": log_content, \"status\": \"success\"}), 200\n    except FileNotFoundError:\n        return jsonify({\"error\": \"Log file not found\"}), 404\n    except Exception as e:\n        return jsonify({\"error\": \"Unable to retrieve logs\"}), 500"}