{"title":"Import Hijacking via sys.path Manipulation","language":"Python","severity":"Critical","cwe":"CWE-427","source_lines":[9],"flow_lines":[9,11],"sink_lines":[11],"vulnerable_code":"import sys\nimport os\n\ndef load_crypto_module(plugin_dir, algo_name):\n    user_path = os.path.join(\"/opt/hsm/plugins\", plugin_dir)\n    sys.path.insert(0, user_path)\n    import importlib\n    crypto_mod = importlib.import_module(algo_name)\n    result = crypto_mod.encrypt(b\"sensitive_key_material\")\n    return result","explanation":"Both `plugin_dir` and `algo_name` are attacker-controlled parameters passed directly into `os.path.join` and `sys.path.insert(0, ...)`, then consumed by `importlib.import_module`. Because `sys.path.insert(0, ...)` prepends the attacker-supplied directory, any module name (including built-ins like `hashlib`) will be resolved from the malicious directory first, allowing arbitrary code execution when the module is imported and its `encrypt()` function is called on sensitive key material.","remediation":"The fix eliminates sys.path manipulation entirely and instead loads modules directly from a validated file path using importlib.util.spec_from_file_location. It enforces three layers of defense: (1) plugin_dir is restricted to simple alphanumeric names via regex and the resolved path is verified to remain under the base directory using os.path.realpath, (2) algo_name must appear in an explicit whitelist of allowed algorithm modules, and (3) the module file's existence is confirmed before loading, preventing path traversal and import hijacking.","secure_code":"import sys\nimport os\nimport re\nimport importlib\nimport importlib.util\n\n# Whitelist of allowed algorithm module names\nALLOWED_ALGORITHMS = frozenset([\"aes_plugin\", \"rsa_plugin\", \"ecdsa_plugin\", \"chacha20_plugin\"])\nBASE_PLUGIN_DIR = \"/opt/hsm/plugins\"\n\n\ndef load_crypto_module(plugin_dir, algo_name):\n    # 1. Validate plugin_dir: allow only simple directory names (no path traversal)\n    if not re.fullmatch(r\"[A-Za-z0-9_\\-]+\", plugin_dir):\n        raise ValueError(\n            f\"Invalid plugin directory name: {plugin_dir!r}. \"\n            \"Only alphanumeric characters, hyphens, and underscores are allowed.\"\n        )\n\n    # 2. Validate algo_name: must be in the explicit whitelist\n    if algo_name not in ALLOWED_ALGORITHMS:\n        raise ValueError(\n            f\"Algorithm {algo_name!r} is not in the list of allowed modules: \"\n            f\"{sorted(ALLOWED_ALGORITHMS)}\"\n        )\n\n    # 3. Resolve the full path and confirm it stays within the base directory\n    user_path = os.path.realpath(os.path.join(BASE_PLUGIN_DIR, plugin_dir))\n    base_real = os.path.realpath(BASE_PLUGIN_DIR)\n    if not user_path.startswith(base_real + os.sep):\n        raise ValueError(\n            f\"Resolved plugin path {user_path!r} escapes the allowed base directory.\"\n        )\n\n    # 4. Verify the target module file actually exists in the expected directory\n    module_file = os.path.join(user_path, algo_name + \".py\")\n    if not os.path.isfile(module_file):\n        raise FileNotFoundError(\n            f\"Module file not found: {module_file}\"\n        )\n\n    # 5. Load the module directly from the validated file path instead of\n    #    manipulating sys.path (avoids import hijacking entirely)\n    spec = importlib.util.spec_from_file_location(algo_name, module_file)\n    if spec is None or spec.loader is None:\n        raise ImportError(f\"Cannot create module spec for {module_file}\")\n\n    crypto_mod = importlib.util.module_from_spec(spec)\n    spec.loader.exec_module(crypto_mod)\n\n    # 6. Ensure the loaded module exposes the expected interface\n    if not callable(getattr(crypto_mod, \"encrypt\", None)):\n        raise AttributeError(\n            f\"Module {algo_name!r} does not expose a callable 'encrypt' function.\"\n        )\n\n    result = crypto_mod.encrypt(b\"sensitive_key_material\")\n    return result"}