{"title":"Import Hijacking via sys.path Manipulation","language":"Python","severity":"Critical","cwe":"CWE-427","source_lines":[20],"flow_lines":[20,19,5,12],"sink_lines":[5,12],"vulnerable_code":"import sys\nimport os\n\ndef load_cloud_storage_plugin(plugin_dir, plugin_name):\n    if not os.path.isabs(plugin_dir):\n        plugin_dir = os.path.join('/var/app/plugins', plugin_dir)\n    sys.path.insert(0, plugin_dir)\n    try:\n        storage_module = __import__(plugin_name)\n        return storage_module.CloudStorageAdapter()\n    except ImportError as e:\n        sys.path.insert(0, '/tmp/fallback_plugins')\n        storage_module = __import__(plugin_name)\n        return storage_module.CloudStorageAdapter()\n\ndef sync_iot_telemetry_data(device_id, custom_plugin_path):\n    adapter = load_cloud_storage_plugin(custom_plugin_path, 'aws_s3_handler')\n    telemetry = adapter.fetch_device_metrics(device_id)\n    return adapter.upload_to_bucket(telemetry, f'iot-data/{device_id}')","explanation":"The function accepts an untrusted `custom_plugin_path` parameter that is directly used to manipulate `sys.path` via `sys.path.insert()`, allowing an attacker to control Python's module search path. This enables import hijacking where malicious modules can be loaded via `__import__()`, leading to arbitrary code execution when the imported module is instantiated.","remediation":"The fix eliminates sys.path manipulation entirely by using importlib.util.spec_from_file_location to load modules directly from validated file paths. It enforces an allowlist of permitted plugin names and constrains plugin directories to a specific base path with symlink-aware canonicalization to prevent directory traversal attacks. The fallback to /tmp/fallback_plugins has been removed entirely as it represented an additional untrusted path.","secure_code":"import os\nimport importlib\nimport importlib.util\n\nALLOWED_PLUGIN_BASE_DIR = '/var/app/plugins'\nALLOWED_PLUGIN_NAMES = {'aws_s3_handler', 'azure_blob_handler', 'gcs_handler'}\n\n\ndef _validate_plugin_dir(plugin_dir):\n    \"\"\"Validate that the plugin directory is within the allowed base directory.\"\"\"\n    if not os.path.isabs(plugin_dir):\n        plugin_dir = os.path.join(ALLOWED_PLUGIN_BASE_DIR, plugin_dir)\n    \n    # Resolve to canonical path to prevent directory traversal\n    resolved_dir = os.path.realpath(plugin_dir)\n    allowed_base = os.path.realpath(ALLOWED_PLUGIN_BASE_DIR)\n    \n    if not resolved_dir.startswith(allowed_base + os.sep) and resolved_dir != allowed_base:\n        raise ValueError(\n            f\"Plugin directory '{plugin_dir}' is outside the allowed base directory.\"\n        )\n    \n    if not os.path.isdir(resolved_dir):\n        raise FileNotFoundError(\n            f\"Plugin directory '{resolved_dir}' does not exist.\"\n        )\n    \n    return resolved_dir\n\n\ndef _validate_plugin_name(plugin_name):\n    \"\"\"Validate that the plugin name is in the allowlist and is a safe identifier.\"\"\"\n    if not plugin_name.isidentifier():\n        raise ValueError(f\"Invalid plugin name: '{plugin_name}'\")\n    \n    if plugin_name not in ALLOWED_PLUGIN_NAMES:\n        raise ValueError(\n            f\"Plugin '{plugin_name}' is not in the list of allowed plugins.\"\n        )\n    \n    return plugin_name\n\n\ndef load_cloud_storage_plugin(plugin_dir, plugin_name):\n    \"\"\"Securely load a cloud storage plugin from a validated directory.\"\"\"\n    validated_dir = _validate_plugin_dir(plugin_dir)\n    validated_name = _validate_plugin_name(plugin_name)\n    \n    # Load the module directly from the specific file path without modifying sys.path\n    module_path = os.path.join(validated_dir, f\"{validated_name}.py\")\n    \n    if not os.path.isfile(module_path):\n        raise ImportError(\n            f\"Plugin module '{validated_name}' not found in '{validated_dir}'.\"\n        )\n    \n    # Verify the file is within the validated directory (prevent symlink attacks)\n    resolved_module_path = os.path.realpath(module_path)\n    if not resolved_module_path.startswith(os.path.realpath(validated_dir) + os.sep):\n        raise ValueError(\n            f\"Plugin file resolves outside the allowed directory (possible symlink attack).\"\n        )\n    \n    # Use importlib to load from a specific file without modifying sys.path\n    spec = importlib.util.spec_from_file_location(validated_name, resolved_module_path)\n    if spec is None or spec.loader is None:\n        raise ImportError(f\"Cannot create module spec for '{validated_name}'.\")\n    \n    storage_module = importlib.util.module_from_spec(spec)\n    spec.loader.exec_module(storage_module)\n    \n    if not hasattr(storage_module, 'CloudStorageAdapter'):\n        raise AttributeError(\n            f\"Plugin '{validated_name}' does not define 'CloudStorageAdapter'.\"\n        )\n    \n    return storage_module.CloudStorageAdapter()\n\n\ndef sync_iot_telemetry_data(device_id, custom_plugin_path):\n    \"\"\"Sync IoT telemetry data using a validated cloud storage plugin.\"\"\"\n    # Validate device_id to prevent path injection\n    if not device_id.replace('-', '').replace('_', '').isalnum():\n        raise ValueError(f\"Invalid device ID: '{device_id}'\")\n    \n    adapter = load_cloud_storage_plugin(custom_plugin_path, 'aws_s3_handler')\n    telemetry = adapter.fetch_device_metrics(device_id)\n    return adapter.upload_to_bucket(telemetry, f'iot-data/{device_id}')"}