{"title":"Directory Traversal via Unvalidated File Path Join","language":"Python","severity":"Critical","cwe":"CWE-22","source_lines":[10,12],"flow_lines":[10,13,14],"sink_lines":[14],"vulnerable_code":"from flask import Flask, request, send_file\nimport os\n\napp = Flask(__name__)\nLOG_STORAGE = '/var/iot/device_logs/'\n\n@app.route('/api/v2/device/fetch_diagnostic', methods=['GET'])\ndef fetch_diagnostic():\n    device_id = request.args.get('device_id', 'unknown')\n    log_type = request.args.get('log_type', 'system')\n    log_filename = request.args.get('filename', 'latest.log')\n    device_log_path = os.path.join(LOG_STORAGE, device_id, log_type, log_filename)\n    if os.path.exists(device_log_path):\n        return send_file(device_log_path, as_attachment=True)\n    return {'error': 'Diagnostic log not found'}, 404","explanation":"The application constructs file paths using unsanitized user input from query parameters (device_id, log_type, filename) directly in os.path.join(). An attacker can inject path traversal sequences (../) in any of these parameters to access arbitrary files outside the intended LOG_STORAGE directory, bypassing the directory structure entirely.","remediation":"The fix applies a defense-in-depth approach: first, each user-supplied path component is validated against an allowlist regex that permits only alphanumeric characters, hyphens, underscores, and dots (but not leading dots), rejecting any traversal sequences or special characters. Second, after constructing the path, os.path.realpath() resolves any symbolic links or remaining traversal attempts, and the resolved path is verified to start with the intended LOG_STORAGE base directory. Additionally, os.path.isfile() ensures only regular files are served.","secure_code":"from flask import Flask, request, send_file\nimport os\nimport re\n\napp = Flask(__name__)\nLOG_STORAGE = '/var/iot/device_logs/'\n\ndef is_safe_path_component(component):\n    \"\"\"Validate that a path component contains only safe characters and no traversal sequences.\"\"\"\n    if not component:\n        return False\n    if '..' in component or '/' in component or '\\\\' in component:\n        return False\n    if not re.match(r'^[a-zA-Z0-9_\\-\\.]+$', component):\n        return False\n    if component.startswith('.'):\n        return False\n    return True\n\n@app.route('/api/v2/device/fetch_diagnostic', methods=['GET'])\ndef fetch_diagnostic():\n    device_id = request.args.get('device_id', 'unknown')\n    log_type = request.args.get('log_type', 'system')\n    log_filename = request.args.get('filename', 'latest.log')\n\n    # Validate each path component individually\n    if not is_safe_path_component(device_id):\n        return {'error': 'Invalid device_id'}, 400\n    if not is_safe_path_component(log_type):\n        return {'error': 'Invalid log_type'}, 400\n    if not is_safe_path_component(log_filename):\n        return {'error': 'Invalid filename'}, 400\n\n    device_log_path = os.path.join(LOG_STORAGE, device_id, log_type, log_filename)\n\n    # Resolve to absolute path and verify it stays within LOG_STORAGE\n    resolved_path = os.path.realpath(device_log_path)\n    resolved_base = os.path.realpath(LOG_STORAGE)\n\n    if not resolved_path.startswith(resolved_base + os.sep):\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': 'Diagnostic log not found'}, 404"}