# Import Hijacking via sys.path Manipulation

Language: Python
Severity: Critical
CWE: CWE-427

## Source
9

## Flow
9-11

## Sink
11

## Vulnerable Code
```python
import sys
import os

def load_crypto_module(plugin_dir, algo_name):
    user_path = os.path.join("/opt/hsm/plugins", plugin_dir)
    sys.path.insert(0, user_path)
    import importlib
    crypto_mod = importlib.import_module(algo_name)
    result = crypto_mod.encrypt(b"sensitive_key_material")
    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
```python
import sys
import os
import re
import importlib
import importlib.util

# Whitelist of allowed algorithm module names
ALLOWED_ALGORITHMS = frozenset(["aes_plugin", "rsa_plugin", "ecdsa_plugin", "chacha20_plugin"])
BASE_PLUGIN_DIR = "/opt/hsm/plugins"


def load_crypto_module(plugin_dir, algo_name):
    # 1. Validate plugin_dir: allow only simple directory names (no path traversal)
    if not re.fullmatch(r"[A-Za-z0-9_\-]+", plugin_dir):
        raise ValueError(
            f"Invalid plugin directory name: {plugin_dir!r}. "
            "Only alphanumeric characters, hyphens, and underscores are allowed."
        )

    # 2. Validate algo_name: must be in the explicit whitelist
    if algo_name not in ALLOWED_ALGORITHMS:
        raise ValueError(
            f"Algorithm {algo_name!r} is not in the list of allowed modules: "
            f"{sorted(ALLOWED_ALGORITHMS)}"
        )

    # 3. Resolve the full path and confirm it stays within the base directory
    user_path = os.path.realpath(os.path.join(BASE_PLUGIN_DIR, plugin_dir))
    base_real = os.path.realpath(BASE_PLUGIN_DIR)
    if not user_path.startswith(base_real + os.sep):
        raise ValueError(
            f"Resolved plugin path {user_path!r} escapes the allowed base directory."
        )

    # 4. Verify the target module file actually exists in the expected directory
    module_file = os.path.join(user_path, algo_name + ".py")
    if not os.path.isfile(module_file):
        raise FileNotFoundError(
            f"Module file not found: {module_file}"
        )

    # 5. Load the module directly from the validated file path instead of
    #    manipulating sys.path (avoids import hijacking entirely)
    spec = importlib.util.spec_from_file_location(algo_name, module_file)
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot create module spec for {module_file}")

    crypto_mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(crypto_mod)

    # 6. Ensure the loaded module exposes the expected interface
    if not callable(getattr(crypto_mod, "encrypt", None)):
        raise AttributeError(
            f"Module {algo_name!r} does not expose a callable 'encrypt' function."
        )

    result = crypto_mod.encrypt(b"sensitive_key_material")
    return result
```
