# PYTHON-LANG-SEC-005: Non-literal Dynamic Import Detected

> **Severity:** MEDIUM | **CWE:** CWE-95 | **OWASP:** A03:2021

- **Language:** Python
- **Category:** Python Core
- **URL:** https://codepathfinder.dev/registry/python/lang/PYTHON-LANG-SEC-005
- **Detection:** `pathfinder scan --ruleset python/PYTHON-LANG-SEC-005 --project .`

## Description

Python's __import__() built-in and importlib.import_module() allow modules to be loaded
by name at runtime. When the module name argument is derived from untrusted external input
such as HTTP parameters, configuration files, or user-provided data, an attacker can import
malicious modules, access sensitive built-in modules such as os or subprocess, or perform
path traversal to load modules from unexpected filesystem locations.

Common vulnerable patterns include plugin systems that load user-specified modules by name,
serialization systems that reconstruct classes using stored module paths, and configuration-
driven dispatch tables that resolve handler names from external data.

The fix is always to validate the module name against an explicit allowlist of permitted
module names before calling import_module(), ensuring no user-controlled value can cause
an unexpected module to be loaded.


## Vulnerable Code

```python
import code
import importlib
import typing

# SEC-005: non-literal import
module_name = "os"
mod = __import__(module_name)
mod2 = importlib.import_module(module_name)
```

## Secure Code

```python
import importlib

# SECURE: Validate module name against an explicit allowlist
ALLOWED_PLUGINS = {
    "csv_processor",
    "json_processor",
    "xml_processor",
}

def load_processor(plugin_name: str):
    if plugin_name not in ALLOWED_PLUGINS:
        raise ValueError(f"Plugin not allowed: {plugin_name}")
    module = importlib.import_module(f"myapp.processors.{plugin_name}")
    return module.Processor()

# SECURE: Use a registry pattern to avoid dynamic imports entirely
from myapp.processors import csv_processor, json_processor, xml_processor

PROCESSOR_REGISTRY = {
    "csv": csv_processor.Processor,
    "json": json_processor.Processor,
    "xml": xml_processor.Processor,
}

def get_processor(format_name: str):
    if format_name not in PROCESSOR_REGISTRY:
        raise ValueError(f"Unknown format: {format_name}")
    return PROCESSOR_REGISTRY[format_name]()

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets


@python_rule(
    id="PYTHON-LANG-SEC-005",
    name="Non-literal import Detected",
    severity="MEDIUM",
    category="lang",
    cwe="CWE-95",
    tags="python,import,dynamic-import,CWE-95",
    message="__import__() or importlib.import_module() with non-literal argument detected.",
    owasp="A03:2021",
)
def detect_non_literal_import():
    """Detects dynamic imports via __import__ and importlib."""
    return calls("__import__", "importlib.import_module")
```

## How to Fix

- Always validate the module name against an explicit allowlist before calling import_module() or __import__().
- Prefer a static import registry (dict mapping names to already-imported classes) over dynamic imports to avoid the risk entirely.
- Restrict plugin directories and sys.path to prevent loading of modules outside the intended scope.
- Use __import__() only with hardcoded string literals; use importlib.import_module() for dynamic imports with proper validation.
- Log all dynamic module imports with the caller context to detect attempts to load unauthorized modules.

## Security Implications

- **Arbitrary Module Execution:** An attacker who controls the module name can import os, subprocess, socket, or any
other standard library module and immediately call dangerous functions on it. Module-level
code is executed on import, so importing a malicious module can trigger code execution
before any function is called.

- **Plugin System Exploitation:** Plugin systems using import_module() to load user-specified plugins are vulnerable to
loading attacker-controlled modules from the Python path. If the attacker can place a
file on the server or influence sys.path, they can execute arbitrary code through the
plugin loader.

- **Pickle-like Class Reconstruction Attacks:** Serialization formats that store class locations as module:classname strings and use
import_module() to reconstruct them are vulnerable to the same class of attacks as
pickle deserialization. An attacker who controls the stored class location can cause
any callable to be invoked during deserialization.

- **Path Traversal to Sensitive Modules:** Depending on sys.path configuration, an attacker may be able to use relative module
names or dotted paths to traverse to modules outside the intended plugin directory,
potentially loading internal utility modules that expose sensitive operations.


## FAQ

**Q: Is all dynamic importing dangerous or only when using untrusted input?**

Dynamic importing is only dangerous when the module name can be influenced by untrusted
input. Plugin systems that load modules from a fixed list of developer-defined names are
safe. The risk arises when an HTTP parameter, file path, or environment variable value
reaches the module name argument without validation.


**Q: What is the difference between __import__() and importlib.import_module()?**

Both ultimately load Python modules, but importlib.import_module() is the recommended
public API for dynamic imports since Python 3. __import__() is the lower-level built-in
that import statements compile to. Both are equally dangerous when called with untrusted
arguments. Prefer importlib.import_module() with an allowlist when dynamic loading is
genuinely required.


**Q: Can an attacker import standard library modules and use them for attacks?**

Yes. Standard library modules such as os, subprocess, socket, shutil, and ctypes all
provide capabilities that can be used for attacks. Importing os and calling os.system()
or os.environ is a common technique for exploiting dynamic import vulnerabilities. An
allowlist must not include any module with dangerous capabilities.


**Q: Does this rule flag importlib usage in package __init__.py files?**

Yes, all non-literal dynamic imports are flagged. Package initialization code that uses
importlib to load sub-modules can typically be rewritten with explicit static imports or
a module registry pattern. Suppressed findings should document the trust boundary and
explain why the module name cannot be attacker-controlled.


**Q: What about pickle and similar serializers that use import_module() internally?**

Serializers that reconstruct objects by dynamically importing their class module are
vulnerable to this class of attack when the serialized data comes from untrusted sources.
See PYTHON-LANG-SEC-040 through PYTHON-LANG-SEC-046 for deserialization-specific rules.


**Q: How should I handle plugin systems that genuinely need dynamic imports?**

Maintain an allowlist of permitted plugin module names, ideally populated from a
developer-controlled configuration file (not user input). Validate the requested plugin
name against this list before calling import_module(). Consider using Python's entry_points
mechanism (setuptools) for plugin discovery, which limits loading to installed packages.


## References

- [CWE-95: Eval Injection](https://cwe.mitre.org/data/definitions/95.html)
- [Python docs: importlib.import_module()](https://docs.python.org/3/library/importlib.html#importlib.import_module)
- [Python docs: built-in __import__()](https://docs.python.org/3/library/functions.html#import__)
- [OWASP Top 10 A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/)
- [Python Import System Documentation](https://docs.python.org/3/reference/import.html)

---

Source: https://codepathfinder.dev/registry/python/lang/PYTHON-LANG-SEC-005
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
