{"title":"Path Traversal via os.path.join on Untrusted Filename","language":"Python","severity":"High","cwe":"CWE-22","source_lines":[8,9],"flow_lines":[8,9,11,12],"sink_lines":[12],"vulnerable_code":"import os\nfrom flask import Flask, request, send_file\n\napp = Flask(__name__)\n\n@app.route('/api/iot/firmware/download', methods=['GET'])\ndef retrieve_firmware_binary():\n    device_model = request.args.get('model', 'default')\n    fw_version = request.args.get('version', 'latest')\n    firmware_base = '/var/iot/firmware_repository'\n    fw_filename = f\"{device_model}_{fw_version}.bin\"\n    full_fw_path = os.path.join(firmware_base, fw_filename)\n    if os.path.exists(full_fw_path):\n        return send_file(full_fw_path, as_attachment=True)\n    return {'error': 'Firmware not found'}, 404","explanation":"User-controlled parameters 'model' and 'version' are concatenated into a filename without validation, allowing path traversal sequences like '../'. When passed to os.path.join() and subsequently send_file(), an attacker can traverse directories to access arbitrary files on the filesystem.","remediation":"The fix applies two layers of defense: first, it validates that both user inputs (model and version) contain only safe alphanumeric characters, hyphens, underscores, and dots using a regex whitelist, rejecting any path traversal characters. Second, it resolves the constructed path to its canonical absolute form using os.path.realpath() and verifies it remains within the intended firmware base directory, preventing any bypass through symlinks or encoded traversal sequences.","secure_code":"import os\nimport re\nfrom flask import Flask, request, send_file\n\napp = Flask(__name__)\n\n@app.route('/api/iot/firmware/download', methods=['GET'])\ndef retrieve_firmware_binary():\n    device_model = request.args.get('model', 'default')\n    fw_version = request.args.get('version', 'latest')\n    firmware_base = '/var/iot/firmware_repository'\n\n    # Validate that model and version contain only safe characters (alphanumeric, hyphens, underscores, dots)\n    safe_pattern = re.compile(r'^[a-zA-Z0-9._-]+$')\n    if not safe_pattern.match(device_model) or not safe_pattern.match(fw_version):\n        return {'error': 'Invalid model or version parameter'}, 400\n\n    fw_filename = f\"{device_model}_{fw_version}.bin\"\n    full_fw_path = os.path.join(firmware_base, fw_filename)\n\n    # Resolve to absolute path and verify it stays within the firmware base directory\n    resolved_path = os.path.realpath(full_fw_path)\n    if not resolved_path.startswith(os.path.realpath(firmware_base) + os.sep):\n        return {'error': 'Invalid firmware path'}, 400\n\n    if os.path.exists(resolved_path):\n        return send_file(resolved_path, as_attachment=True)\n    return {'error': 'Firmware not found'}, 404"}