{"title":"Path Traversal","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"from flask import Flask, request, send_file\nimport os\napp = Flask(__name__)\nSATELLITE_DATA_DIR = '/opt/telemetry/satellite_feeds'\n@app.route('/api/iot/fetch_log')\ndef fetch_device_log():\n    dev_id = request.args.get('device_id')\n    log_name = request.args.get('log_file')\n    resolved_path = os.path.join(SATELLITE_DATA_DIR, dev_id, log_name)\n    if not os.path.exists(resolved_path):\n        return {'error': 'log not found'}, 404\n    return send_file(resolved_path)","explanation":"Both `dev_id` (line 7) and `log_name` (line 8) are taken directly from user-supplied query parameters without any sanitization or validation. These two untrusted inputs are chained together via `os.path.join` (line 9) to construct a filesystem path, which is then passed to `send_file` (line 11). Because `os.path.join` does not normalize or restrict path traversal sequences, an attacker can supply `../` sequences in either parameter to escape the intended `SATELLITE_DATA_DIR` base directory and read arbitrary files on the server.","remediation":"The fix applies a defense-in-depth strategy: first, both user-supplied parameters (`device_id` and `log_file`) are validated to reject path separators, traversal sequences (`..`), and null bytes. Second, after constructing the joined path, `os.path.realpath()` is used to resolve symlinks and normalize the path, and then a strict prefix check ensures the resolved path remains within the `SATELLITE_DATA_DIR` base directory. Additionally, `os.path.isfile()` replaces `os.path.exists()` to ensure only regular files (not directories) are served.","secure_code":"from flask import Flask, request, send_file, abort\nimport os\n\napp = Flask(__name__)\nSATELLITE_DATA_DIR = '/opt/telemetry/satellite_feeds'\n\n\ndef is_safe_path_component(component):\n    \"\"\"Validate that a path component does not contain traversal sequences or absolute paths.\"\"\"\n    if not component:\n        return False\n    # Reject any component containing path separators or traversal sequences\n    if os.sep in component or '/' in component or '\\\\' in component:\n        return False\n    if component in ('.', '..'):\n        return False\n    if '\\x00' in component:\n        return False\n    return True\n\n\n@app.route('/api/iot/fetch_log')\ndef fetch_device_log():\n    dev_id = request.args.get('device_id')\n    log_name = request.args.get('log_file')\n\n    # Reject missing parameters\n    if not dev_id or not log_name:\n        return {'error': 'device_id and log_file are required'}, 400\n\n    # Validate individual path components to block traversal characters\n    if not is_safe_path_component(dev_id) or not is_safe_path_component(log_name):\n        return {'error': 'invalid device_id or log_file parameter'}, 400\n\n    # Build the candidate path and resolve to an absolute, symlink-free path\n    resolved_path = os.path.realpath(\n        os.path.join(SATELLITE_DATA_DIR, dev_id, log_name)\n    )\n\n    # Ensure the resolved path is strictly within the allowed base directory\n    base_dir = os.path.realpath(SATELLITE_DATA_DIR) + os.sep\n    if not resolved_path.startswith(base_dir):\n        return {'error': 'access denied'}, 403\n\n    if not os.path.isfile(resolved_path):\n        return {'error': 'log not found'}, 404\n\n    return send_file(resolved_path)"}