Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Dynamic imports are especially dangerous if you cannot validate upfront what is being imported.

Dynamic imports can be achieved using:

Security concerns

Dynamic imports present a potential security issue, particularly when the module name originates from an untrusted source. Common risky scenarios include:

Why importlib.import_module() is preferred over __import__

Aspect__import__importlib.import_module()
PurposeLow-level, legacy functionModern, explicit API
ReadabilityObscure, looks like a magic methodClear, self-documenting name
MaintainabilityDiscouraged for general useActively maintained by core Python developers
SecurityNo built-in safeguardsSame risks but cleaner interface

Module imports in Python Plugin systems

From a security-by-design perspective, utilizing importlib.util.spec_from_file_location() alongside module_from_spec() to load plugins introduces severe architectural security risks. This mechanism operates at a low level, bypassing Python’s standard, sandboxed import restrictions and allowing the direct execution of arbitrary code from any accessible file path on the disk.

If the file paths or module specifications are influenced by untrusted input—such as user-controlled configuration files or unvalidated directory paths—an attacker can exploit this to achieve Remote Code Execution (RCE) by tricking the application into importing malicious scripts.

Rather than pointing directly to unpredictable locations on disk, modern Python architectures should leverage namespace packages or the standard importlib.metadata entry points API. By registering plugins via package metadata, the application shifts from a dangerous “pull-from-disk” model to a secure, declarative system where only explicitly installed distribution packages can be loaded.

Mitigations

There is always a security risk when importlib.import_module() is used. No mitigation eliminates this risk entirely — it can only be reduced.

Possible mitigations:

  1. Use static analysis tools: Always use a Python code audit tool with module scanning options for all modules within a file.

  1. Pre-audit all possible imports : Check and understand what will be imported and what security risks are involved. You MUST never trust that dynamic imports are safe. Most are not!

  1. Review API design – Check whether your Python program has or truly needs an API to download or import modules dynamically. Often, a static approach can replace the dynamic one.

  1. Seek expert review – If you do not trust the implementation, call a security expert to help you. See the sponsor page for companies that can assist.

Example

Insecure Dynamic Import

# INSECURE: Directly importing user-supplied module name
import importlib

user_input = input("Enter module name to load: ")
module = importlib.import_module(user_input)

# If user enters "os", they can now access system functions
# If user enters "subprocess", they can execute shell commands
module.system("rm -rf /")  # Potentially catastrophic

What is wrong with this code?

The Vulnerable Pattern for a plugin system

# SECURITY RISK: Avoid this low-level dynamic loading pattern
spec = importlib.util.spec_from_file_location(plugin_name, unsafe_file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)  # Arbitrary code execution occurs here

The most secure approach is to avoid dynamic imports entirely and use static imports with dispatch tables:

# MOST SECURE: No dynamic imports - use static imports with dispatch
import math
import json
import datetime
import re
from typing import Dict, Callable, Any

# Dispatch table mapping names to actual modules
MODULE_DISPATCH: Dict[str, Any] = {
    "math": math,
    "json": json,
    "datetime": datetime,
    "re": re
}

# Dispatch table for specific functions
FUNCTION_DISPATCH: Dict[str, Callable] = {
    "math.sqrt": math.sqrt,
    "math.pi": lambda: math.pi,
    "json.loads": json.loads,
    "json.dumps": json.dumps,
    "datetime.datetime": datetime.datetime,
    "re.search": re.search
}

def get_module(module_name: str) -> Any:
    """Get a module from the dispatch table - no dynamic import needed."""
    if module_name not in MODULE_DISPATCH:
        raise ValueError(f"Module '{module_name}' not available")
    return MODULE_DISPATCH[module_name]

def get_function(function_name: str) -> Callable:
    """Get a function from the dispatch table - no dynamic import needed."""
    if function_name not in FUNCTION_DISPATCH:
        raise ValueError(f"Function '{function_name}' not available")
    return FUNCTION_DISPATCH[function_name]

# Usage
user_input = "math.sqrt"  # From user input
try:
    func = get_function(user_input)
    result = func(25)  # sqrt(25) = 5
    print(f"Result: {result}")
except ValueError as e:
    print(f"Invalid request: {e}")

Summary of Best Practices

PracticeRecommendation
Avoid dynamic importsPrefer static imports with dispatch tables whenever possible
Never use __import__()This legacy function should never appear in modern Python code
Use allowlistsIf dynamic imports are unavoidable, maintain a strict allowlist of permitted modules
Validate function accessDo not expose entire modules — restrict access to specific safe functions
Audit all importsUse static analysis tools to detect and review all dynamic import usage

Security and Python plugin systems

MethodDynamic discoveryLoads arbitrary filesRecommended
importlib.metadata.entry_points()YesNo⭐⭐⭐⭐⭐
importlib.import_module()LimitedNo⭐⭐⭐⭐☆
pkgutil.iter_modules() + import_module()YesNo⭐⭐⭐⭐☆
Namespace packagesYesNo⭐⭐⭐⭐☆
spec_from_file_location()YesYes⭐⭐☆☆☆
SourceFileLoaderYesYes⭐⭐☆☆☆
__import__()NoNo⭐⭐⭐☆☆

For new applications, the most robust and secure options are:

  1. importlib.metadata.entry_points() when plugins are installed as Python packages. This leverages Python’s packaging ecosystem and avoids arbitrary file loading.

  2. pkgutil.iter_modules() + importlib.import_module() when plugins reside in a known package within your application. This provides automatic discovery while still using Python’s standard import machinery.

  3. Reserve importlib.util.spec_from_file_location() for cases where you genuinely need to load modules from arbitrary file paths (for example, user-provided scripts in a controlled environment). If you use it, ensure the plugin directory is trusted, validate or authenticate plugin files (for example, using signatures or hashes), and avoid passing user-controlled paths directly to the loader.

From a security perspective, spec_from_file_location() and related APIs (module_from_spec(), exec_module()) are always worth reviewing as this methods can bypass the normal import mechanism and can execute arbitrary Python files. By contrast, import_module(), pkgutil.iter_modules(), and importlib.metadata.entry_points() generally represent a lower-risk patterns because they work within Python’s standard import and packaging systems.

References