diff --git a/packages/@n8n/task-runner-python/src/task_executor.py b/packages/@n8n/task-runner-python/src/task_executor.py index 68a35db8d7f..5e6890c0512 100644 --- a/packages/@n8n/task-runner-python/src/task_executor.py +++ b/packages/@n8n/task-runner-python/src/task_executor.py @@ -405,8 +405,12 @@ class TaskExecutor: else: safe_modules.update(external_allow) + # keep modules marked as safe and submodules of those modules_to_remove = [ - name for name in sys.modules.keys() if name not in safe_modules + name + for name in sys.modules.keys() + if name not in safe_modules + and not any(name.startswith(safe + ".") for safe in safe_modules) ] for module_name in modules_to_remove: diff --git a/packages/@n8n/task-runner-python/tests/fixtures/task_runner_manager.py b/packages/@n8n/task-runner-python/tests/fixtures/task_runner_manager.py index 7e21c11ebcb..fb0710b2549 100644 --- a/packages/@n8n/task-runner-python/tests/fixtures/task_runner_manager.py +++ b/packages/@n8n/task-runner-python/tests/fixtures/task_runner_manager.py @@ -27,9 +27,11 @@ class TaskRunnerManager: self, task_broker_url: str = LOCAL_TASK_BROKER_URL, graceful_shutdown_timeout: float | None = None, + custom_env: dict[str, str] | None = None, ): self.task_broker_url = task_broker_url self.graceful_shutdown_timeout = graceful_shutdown_timeout + self.custom_env = custom_env or {} self.subprocess: asyncio.subprocess.Process | None = None self.stdout_buffer: list[str] = [] self.stderr_buffer: list[str] = [] @@ -49,6 +51,7 @@ class TaskRunnerManager: self.graceful_shutdown_timeout ) env_vars["PYTHONPATH"] = str(project_root) + env_vars.update(self.custom_env) self.subprocess = await asyncio.create_subprocess_exec( sys.executable, diff --git a/packages/@n8n/task-runner-python/tests/integration/conftest.py b/packages/@n8n/task-runner-python/tests/integration/conftest.py index 753cc9ccaa1..f6ecf8f97ce 100644 --- a/packages/@n8n/task-runner-python/tests/integration/conftest.py +++ b/packages/@n8n/task-runner-python/tests/integration/conftest.py @@ -27,6 +27,18 @@ async def broker(): await broker.stop() +@pytest_asyncio.fixture +async def manager_with_stdlib_wildcard(): + manager = TaskRunnerManager( + custom_env={ + "N8N_RUNNERS_STDLIB_ALLOW": "*", + } + ) + await manager.start() + yield manager + await manager.stop() + + def create_task_settings( code: str, node_mode: str, diff --git a/packages/@n8n/task-runner-python/tests/integration/test_execution.py b/packages/@n8n/task-runner-python/tests/integration/test_execution.py index 895ebf5c90b..03d6ff10af0 100644 --- a/packages/@n8n/task-runner-python/tests/integration/test_execution.py +++ b/packages/@n8n/task-runner-python/tests/integration/test_execution.py @@ -230,3 +230,19 @@ async def test_timeout_during_execution(broker, manager): assert error_msg["taskId"] == task_id assert "timed out" in error_msg["error"]["message"].lower() + + +@pytest.mark.asyncio +async def test_stdlib_submodules_with_wildcard(broker, manager_with_stdlib_wildcard): + task_id = nanoid() + code = textwrap.dedent(""" + from collections.abc import Iterable + result = isinstance([1, 2, 3], Iterable) + return [{"json": {"is_iterable": result}}] + """) + task_settings = create_task_settings(code=code, node_mode="all_items") + await broker.send_task(task_id=task_id, task_settings=task_settings) + + result = await wait_for_task_done(broker, task_id) + + assert result["data"]["result"] == [{"json": {"is_iterable": True}}]