{"title":"Import Hijacking via Untrusted Module Resolution","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[16,17],"flow_lines":[16,17,18,4,6,9],"sink_lines":[6,9],"vulnerable_code":"import sys\nimport os\n\ndef load_cloud_credentials(provider_name, config_dir):\n    credential_path = os.path.join(config_dir, 'plugins')\n    sys.path.insert(0, credential_path)\n    try:\n        cloud_module = __import__(f'{provider_name}_auth')\n        authenticator = cloud_module.CloudAuthenticator()\n        session = authenticator.get_session()\n        return session\n    except ImportError:\n        return None\n\ndef deploy_to_cloud(service_config):\n    user_provider = service_config.get('cloud_provider', 'aws')\n    plugin_dir = service_config.get('plugin_directory', '/tmp/cloud_plugins')\n    session = load_cloud_credentials(user_provider, plugin_dir)\n    return session.deploy(service_config['manifest'])","explanation":"The application accepts user-controlled input for both the plugin directory path and cloud provider module name, then inserts this path at the beginning of sys.path and dynamically imports modules. An attacker can specify a malicious directory containing a crafted Python module that will be imported and executed with the application's privileges, leading to arbitrary code execution.","remediation":"The fix removes user control over both the plugin directory and module name by enforcing an allowlist of known cloud providers and loading modules only from a hardcoded trusted directory using importlib.util.spec_from_file_location (avoiding sys.path manipulation). Path traversal is prevented by resolving the real path and verifying it remains within the trusted directory.","secure_code":"import sys\nimport os\nimport importlib\nimport importlib.util\n\n# Allowlist of trusted cloud providers\nALLOWED_PROVIDERS = {'aws', 'azure', 'gcp'}\n\n# Trusted plugin directory (not user-configurable)\nTRUSTED_PLUGIN_DIR = '/opt/cloud_deploy/plugins'\n\n\ndef _validate_provider_name(provider_name):\n    \"\"\"Validate that the provider name is in the allowlist.\"\"\"\n    if provider_name not in ALLOWED_PROVIDERS:\n        raise ValueError(\n            f\"Unsupported cloud provider: '{provider_name}'. \"\n            f\"Allowed providers: {', '.join(sorted(ALLOWED_PROVIDERS))}\"\n        )\n    return provider_name\n\n\ndef load_cloud_credentials(provider_name):\n    \"\"\"Load cloud credentials from a trusted, fixed plugin directory.\"\"\"\n    # Validate provider name against allowlist\n    validated_provider = _validate_provider_name(provider_name)\n\n    module_name = f'{validated_provider}_auth'\n    module_file = f'{module_name}.py'\n    module_path = os.path.join(TRUSTED_PLUGIN_DIR, module_file)\n\n    # Resolve to absolute path and verify it stays within trusted directory\n    module_path = os.path.realpath(module_path)\n    trusted_dir = os.path.realpath(TRUSTED_PLUGIN_DIR)\n\n    if not module_path.startswith(trusted_dir + os.sep):\n        raise ValueError(\"Module path escapes trusted plugin directory.\")\n\n    if not os.path.isfile(module_path):\n        return None\n\n    # Load module from specific file path without modifying sys.path\n    try:\n        spec = importlib.util.spec_from_file_location(module_name, module_path)\n        if spec is None or spec.loader is None:\n            return None\n        cloud_module = importlib.util.module_from_spec(spec)\n        spec.loader.exec_module(cloud_module)\n        authenticator = cloud_module.CloudAuthenticator()\n        session = authenticator.get_session()\n        return session\n    except (ImportError, AttributeError, OSError):\n        return None\n\n\ndef deploy_to_cloud(service_config):\n    user_provider = service_config.get('cloud_provider', 'aws')\n    # plugin_directory from config is intentionally ignored; only trusted dir is used\n    session = load_cloud_credentials(user_provider)\n    if session is None:\n        raise RuntimeError(f\"Failed to load credentials for provider: {user_provider}\")\n    return session.deploy(service_config['manifest'])"}