# Import Hijacking via sys.path Manipulation

Language: Python
Severity: Critical
CWE: CWE-426

## Source
4

## Flow
4-5-7

## Sink
7

## Vulnerable Code
```python
import sys
import importlib
def load_sensor_plugin(plugin_dir, sensor_type):
    usr_path = plugin_dir.strip()
    sys.path.insert(0, usr_path)
    try:
        mod = importlib.import_module(f'sensor_{sensor_type}')
        handler = getattr(mod, 'SensorHandler')
        return handler()
    except (ImportError, AttributeError) as e:
        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
```python
import sys
import os
import importlib
import importlib.util

# Define an allowlist of trusted plugin directories (absolute, canonical paths)
ALLOWED_PLUGIN_DIRS = [
    os.path.realpath('/opt/iot-gateway/plugins'),
    os.path.realpath('/usr/lib/iot-gateway/sensors'),
]

# Define an allowlist of valid sensor type names
ALLOWED_SENSOR_TYPES = frozenset({
    'temperature', 'humidity', 'pressure', 'motion', 'light', 'gas',
})


def load_sensor_plugin(plugin_dir, sensor_type):
    # 1. Validate sensor_type against a strict allowlist
    if sensor_type not in ALLOWED_SENSOR_TYPES:
        return {'error': f'Unknown sensor type: {sensor_type!r}'}

    # 2. Canonicalize the requested directory and verify it is in the allowlist
    canonical_dir = os.path.realpath(plugin_dir.strip())
    if canonical_dir not in ALLOWED_PLUGIN_DIRS:
        return {'error': f'Plugin directory not in allowlist: {canonical_dir!r}'}

    if not os.path.isdir(canonical_dir):
        return {'error': f'Plugin directory does not exist: {canonical_dir!r}'}

    # 3. Build the expected module file path and verify it exists inside the
    #    allowed directory (prevents path-traversal via crafted sensor_type).
    module_name = f'sensor_{sensor_type}'
    module_file = os.path.join(canonical_dir, f'{module_name}.py')
    real_module_file = os.path.realpath(module_file)

    if not real_module_file.startswith(canonical_dir + os.sep):
        return {'error': 'Resolved module path escapes the plugin directory'}

    if not os.path.isfile(real_module_file):
        return {'error': f'Plugin file not found: {real_module_file!r}'}

    # 4. Load the specific file directly via importlib.util instead of
    #    manipulating sys.path, so no other code can be shadowed.
    try:
        spec = importlib.util.spec_from_file_location(module_name, real_module_file)
        if spec is None or spec.loader is None:
            return {'error': f'Cannot create import spec for {real_module_file!r}'}

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

        handler_cls = getattr(mod, 'SensorHandler', None)
        if handler_cls is None:
            return {'error': f'SensorHandler class not found in {module_name}'}

        return handler_cls()
    except Exception as e:
        return {'error': str(e)}
```
