{"title":"Import Hijacking via sys.path Shadowing","language":"Python","severity":"Critical","cwe":"CWE-427","source_lines":[4,5],"flow_lines":[4,5,6],"sink_lines":[6],"vulnerable_code":"import sys\nimport os\ndef load_cloud_credentials(bucket_name, plugin_dir):\n    user_plugin_path = os.path.join('/var/app/plugins', plugin_dir)\n    sys.path.insert(0, user_plugin_path)\n    import boto3\n    s3_client = boto3.client('s3', aws_access_key_id=os.getenv('AWS_KEY'), aws_secret_access_key=os.getenv('AWS_SECRET'))\n    response = s3_client.list_objects_v2(Bucket=bucket_name)\n    return response['Contents']\ndef initialize_storage(tenant_id, custom_plugin):\n    bucket = f'tenant-{tenant_id}-storage'\n    return load_cloud_credentials(bucket, custom_plugin)","explanation":"The code inserts an untrusted user-controlled plugin directory at the beginning of sys.path before importing boto3. A malicious tenant can place a fake boto3.py module in their plugin directory to shadow the legitimate library, intercepting AWS credentials passed during client initialization.","remediation":"The fix imports boto3 at module level before any user-controlled path manipulation can occur, completely eliminating the possibility of import shadowing. Additionally, sys.path is never modified with user-controlled directories; instead, plugins are loaded explicitly using importlib.util.spec_from_file_location for isolated module loading. A validation layer checks plugin directories for path traversal and scans for files that would shadow critical libraries.","secure_code":"import sys\nimport os\nimport importlib\nimport importlib.util\n\n# Import boto3 at module level BEFORE any path manipulation\nimport boto3\n\nALLOWED_PLUGIN_PREFIX = '/var/app/plugins'\nALLOWED_PLUGIN_MODULES = set()  # Whitelist of allowed plugin module names\nBLOCKED_MODULE_NAMES = {'boto3', 'botocore', 'os', 'sys', 'subprocess', 'shutil', 'socket', 'http', 'urllib'}\n\n\ndef _validate_plugin_dir(plugin_dir):\n    \"\"\"Validate and sanitize the plugin directory path.\"\"\"\n    # Prevent path traversal\n    sanitized = os.path.normpath(plugin_dir)\n    if '..' in sanitized or sanitized.startswith('/'):\n        raise ValueError(f\"Invalid plugin directory: {plugin_dir}\")\n    \n    full_path = os.path.realpath(os.path.join(ALLOWED_PLUGIN_PREFIX, sanitized))\n    \n    # Ensure resolved path is still under the allowed prefix\n    if not full_path.startswith(os.path.realpath(ALLOWED_PLUGIN_PREFIX)):\n        raise ValueError(f\"Plugin directory escapes allowed prefix: {plugin_dir}\")\n    \n    if not os.path.isdir(full_path):\n        raise ValueError(f\"Plugin directory does not exist: {full_path}\")\n    \n    return full_path\n\n\ndef _check_no_shadowing(plugin_path):\n    \"\"\"Ensure the plugin directory does not contain modules that shadow critical libraries.\"\"\"\n    if not os.path.isdir(plugin_path):\n        return\n    for entry in os.listdir(plugin_path):\n        module_name = entry.replace('.py', '').replace('.pyc', '')\n        if os.path.isdir(os.path.join(plugin_path, entry)):\n            module_name = entry\n        if module_name in BLOCKED_MODULE_NAMES:\n            raise SecurityError(\n                f\"Plugin directory contains blocked module name: {module_name}\"\n            )\n\n\nclass SecurityError(Exception):\n    \"\"\"Raised when a security violation is detected.\"\"\"\n    pass\n\n\ndef load_plugin_safely(plugin_path, module_name):\n    \"\"\"Load a specific plugin module safely without modifying sys.path.\"\"\"\n    if module_name in BLOCKED_MODULE_NAMES:\n        raise SecurityError(f\"Cannot load blocked module name: {module_name}\")\n    \n    module_file = os.path.join(plugin_path, f\"{module_name}.py\")\n    if not os.path.isfile(module_file):\n        raise FileNotFoundError(f\"Plugin module not found: {module_file}\")\n    \n    spec = importlib.util.spec_from_file_location(module_name, module_file)\n    module = importlib.util.module_from_spec(spec)\n    spec.loader.exec_module(module)\n    return module\n\n\ndef load_cloud_credentials(bucket_name, plugin_dir):\n    \"\"\"Load cloud credentials and list bucket contents.\n    \n    boto3 is imported at module level to prevent shadowing.\n    Plugin directories are validated and never prepended to sys.path.\n    \"\"\"\n    # Validate plugin directory\n    validated_plugin_path = _validate_plugin_dir(plugin_dir)\n    \n    # Check for module shadowing attempts\n    _check_no_shadowing(validated_plugin_path)\n    \n    # Use the already-imported boto3 (imported at module level)\n    s3_client = boto3.client(\n        's3',\n        aws_access_key_id=os.getenv('AWS_KEY'),\n        aws_secret_access_key=os.getenv('AWS_SECRET')\n    )\n    response = s3_client.list_objects_v2(Bucket=bucket_name)\n    return response.get('Contents', [])\n\n\ndef initialize_storage(tenant_id, custom_plugin):\n    \"\"\"Initialize storage for a tenant with validated inputs.\"\"\"\n    # Validate tenant_id to prevent injection\n    if not tenant_id.isalnum():\n        raise ValueError(f\"Invalid tenant_id: {tenant_id}\")\n    \n    bucket = f'tenant-{tenant_id}-storage'\n    return load_cloud_credentials(bucket, custom_plugin)"}