{"title":"Import Hijacking via sys.path Manipulation","language":"Python","severity":"Critical","cwe":"CWE-426","source_lines":[4],"flow_lines":[4,5,7],"sink_lines":[7],"vulnerable_code":"import sys\nimport importlib\ndef load_sensor_plugin(plugin_dir, sensor_type):\n    usr_path = plugin_dir.strip()\n    sys.path.insert(0, usr_path)\n    try:\n        mod = importlib.import_module(f'sensor_{sensor_type}')\n        handler = getattr(mod, 'SensorHandler')\n        return handler()\n    except (ImportError, AttributeError) as e:\n        return {'error': str(e)}","explanation":"The function accepts an attacker-controlled `plugin_dir` parameter, strips it, and immediately inserts it at position 0 of `sys.path` without any validation or allowlist check. This means any attacker-supplied directory (e.g., `/tmp/evil`) will be searched first when `importlib.import_module()` resolves the module name, allowing a malicious `sensor_<type>.py` file placed in that directory to be loaded with full process privileges instead of the legitimate plugin.","remediation":"The fix eliminates sys.path manipulation entirely and instead loads the plugin file directly by its absolute, canonicalized path using importlib.util.spec_from_file_location. Both the plugin directory and the sensor type are validated against strict allowlists before any file I/O or import occurs, and the resolved file path is confirmed to reside within the canonical allowed directory to prevent path-traversal attacks.","secure_code":"import sys\nimport os\nimport importlib\nimport importlib.util\n\n# Define an allowlist of trusted plugin directories (absolute, canonical paths)\nALLOWED_PLUGIN_DIRS = [\n    os.path.realpath('/opt/iot-gateway/plugins'),\n    os.path.realpath('/usr/lib/iot-gateway/sensors'),\n]\n\n# Define an allowlist of valid sensor type names\nALLOWED_SENSOR_TYPES = frozenset({\n    'temperature', 'humidity', 'pressure', 'motion', 'light', 'gas',\n})\n\n\ndef load_sensor_plugin(plugin_dir, sensor_type):\n    # 1. Validate sensor_type against a strict allowlist\n    if sensor_type not in ALLOWED_SENSOR_TYPES:\n        return {'error': f'Unknown sensor type: {sensor_type!r}'}\n\n    # 2. Canonicalize the requested directory and verify it is in the allowlist\n    canonical_dir = os.path.realpath(plugin_dir.strip())\n    if canonical_dir not in ALLOWED_PLUGIN_DIRS:\n        return {'error': f'Plugin directory not in allowlist: {canonical_dir!r}'}\n\n    if not os.path.isdir(canonical_dir):\n        return {'error': f'Plugin directory does not exist: {canonical_dir!r}'}\n\n    # 3. Build the expected module file path and verify it exists inside the\n    #    allowed directory (prevents path-traversal via crafted sensor_type).\n    module_name = f'sensor_{sensor_type}'\n    module_file = os.path.join(canonical_dir, f'{module_name}.py')\n    real_module_file = os.path.realpath(module_file)\n\n    if not real_module_file.startswith(canonical_dir + os.sep):\n        return {'error': 'Resolved module path escapes the plugin directory'}\n\n    if not os.path.isfile(real_module_file):\n        return {'error': f'Plugin file not found: {real_module_file!r}'}\n\n    # 4. Load the specific file directly via importlib.util instead of\n    #    manipulating sys.path, so no other code can be shadowed.\n    try:\n        spec = importlib.util.spec_from_file_location(module_name, real_module_file)\n        if spec is None or spec.loader is None:\n            return {'error': f'Cannot create import spec for {real_module_file!r}'}\n\n        mod = importlib.util.module_from_spec(spec)\n        spec.loader.exec_module(mod)\n\n        handler_cls = getattr(mod, 'SensorHandler', None)\n        if handler_cls is None:\n            return {'error': f'SensorHandler class not found in {module_name}'}\n\n        return handler_cls()\n    except Exception as e:\n        return {'error': str(e)}"}