Skip to content

Commit bbf1320

Browse files
authored
fix: 算子开发中,有代码注入的问题,通过在算子中写入恶意代码可导致RCE (#513)
* fix(security): eliminate shell injection in operator task execution Replace create_subprocess_shell with create_subprocess_exec using argument lists to prevent command injection when executing operator tasks. Added cmd_args parameter to CommandTask/CommandScheduler for safe argument-list-based process spawning. Changes: - cmd_task_scheduler.py: Add cmd_args support to CommandTask and CommandScheduler, using create_subprocess_exec(*cmd_args) which avoids shell interpretation of metacharacters - datamate_wrapper.py: Use cmd_args list instead of shell string - data_juicer_wrapper.py: Use cmd_args list instead of shell string FCE: Code injection via operator shell commands could lead to RCE * fix(security): validate operator names in dynamic module loading Add Python identifier validation to prevent code injection through crafted operator names in importlib.import_module and dynamic imports. Changes: - dataset.py load_ops_module: Validate op_name and registry_content against safe identifier patterns before importlib.import_module - ops/__init__.py: Filter directory names to valid Python identifiers before dynamic import FCE: Malicious operator names could bypass registry and trigger arbitrary code execution via dynamic imports
1 parent 71a0efc commit bbf1320

5 files changed

Lines changed: 42 additions & 15 deletions

File tree

runtime/python-executor/datamate/core/dataset.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import os
6+
import re
67
import importlib
78
import sys
89
import uuid
@@ -129,6 +130,10 @@ def load_ops_module(self, op_name):
129130
:param op_name: 算子名称
130131
:return: 算子对象
131132
'''
133+
# Validate op_name to prevent path traversal / code injection (CodeQL / FCE)
134+
if not op_name or not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', op_name):
135+
raise ValueError(f"Invalid operator name: {op_name}")
136+
132137
parent_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "ops")
133138
if parent_dir not in sys.path:
134139
sys.path.insert(0, parent_dir)
@@ -137,6 +142,9 @@ def load_ops_module(self, op_name):
137142
from core.base_op import OPERATORS as RELATIVE_OPERATORS
138143
registry_content = RELATIVE_OPERATORS.modules.get(op_name)
139144
if isinstance(registry_content, str):
145+
# Validate registry_content is a safe dotted module path (no path separators)
146+
if not re.match(r'^[A-Za-z_][A-Za-z0-9_.]*$', registry_content):
147+
raise ValueError(f"Invalid module path for operator {op_name}: {registry_content}")
140148
# registry_content是module的路径
141149
submodule = importlib.import_module(registry_content)
142150
res = getattr(submodule, op_name, None)

runtime/python-executor/datamate/ops/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import importlib
33
import os
4+
import re
45
import sys
56
from pathlib import Path
67

@@ -13,8 +14,10 @@
1314
# 遍历子目录
1415
for module_name in os.listdir(current_dir):
1516
module_path = os.path.join(current_dir, module_name)
16-
# 检查是否是目录且包含 __init__.py
17-
if os.path.isdir(module_path) and '__init__.py' in os.listdir(module_path):
17+
# 检查是否是目录且包含 __init__.py,且 module_name 为合法的 Python 标识符
18+
if (os.path.isdir(module_path)
19+
and '__init__.py' in os.listdir(module_path)
20+
and re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', module_name)):
1821
# 动态导入模块
1922
try:
2023
importlib.import_module(f".{module_name}", package=__name__)

runtime/python-executor/datamate/scheduler/cmd_task_scheduler.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
class CommandTask(Task):
1212
"""命令任务包装类"""
1313

14-
def __init__(self, task_id: str, command: str, log_path = None, shell: bool = True,
15-
timeout: Optional[int] = None, *args, **kwargs):
14+
def __init__(self, task_id: str, command: str = None, log_path = None, shell: bool = True,
15+
timeout: Optional[int] = None, cmd_args: List[str] = None, *args, **kwargs):
1616
super().__init__(task_id, *args, **kwargs)
1717
self.max_backups = 9
1818
self.log_path = log_path
1919
self.command = command
20-
self.shell = shell
20+
self.cmd_args = cmd_args
21+
self.shell = shell if cmd_args is None else False
2122
self.timeout = timeout
2223
self.return_code = None
2324
self._process = None
@@ -44,8 +45,15 @@ async def _execute(self):
4445
current_log_path = f"{self.log_path}.{counter}"
4546

4647
with open(current_log_path, 'a') as f:
47-
# 使用 asyncio.create_subprocess_shell 或 create_subprocess_exec
48-
if self.shell:
48+
if self.cmd_args is not None:
49+
# Safe: use argument list with create_subprocess_exec (no shell injection)
50+
process = await asyncio.create_subprocess_exec(
51+
*self.cmd_args,
52+
stdout=f,
53+
stderr=asyncio.subprocess.STDOUT,
54+
**self.kwargs
55+
)
56+
elif self.shell:
4957
process = await asyncio.create_subprocess_shell(
5058
self.command,
5159
stdout=f,
@@ -132,7 +140,7 @@ def cancel(self) -> bool:
132140
def to_result(self) -> TaskResult:
133141
"""转换为结果对象"""
134142
self.result = {
135-
"command": self.command,
143+
"command": self.command or self.cmd_args,
136144
"return_code": self.return_code,
137145
}
138146
return super().to_result()
@@ -144,13 +152,13 @@ class CommandScheduler(TaskScheduler):
144152
def __init__(self, max_concurrent: int = 5):
145153
super().__init__(max_concurrent)
146154

147-
async def submit(self, task_id, command: str, log_path = None, shell: bool = True,
148-
timeout: Optional[int] = None, **kwargs) -> str:
155+
async def submit(self, task_id, command: str = None, log_path = None, shell: bool = True,
156+
timeout: Optional[int] = None, cmd_args: List[str] = None, **kwargs) -> str:
149157
if log_path is None:
150158
log_path = f"/flow/{task_id}/output.log"
151159

152160
"""提交命令任务"""
153-
task = CommandTask(task_id, command, log_path, shell, timeout, **kwargs)
161+
task = CommandTask(task_id, command, log_path, shell, timeout, cmd_args=cmd_args, **kwargs)
154162
self.tasks[task_id] = task
155163

156164
# 使用信号量限制并发

runtime/python-executor/datamate/wrappers/data_juicer_wrapper.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66

77
async def submit(task_id, config_path):
88
current_dir = os.path.dirname(__file__)
9+
executor_script = os.path.join(current_dir, 'data_juicer_executor.py')
910

10-
await cmd_scheduler.submit(task_id, f"python {os.path.join(current_dir, 'data_juicer_executor.py')} "
11-
f"--config_path={config_path}")
11+
# Use argument list to avoid shell injection (CodeQL / FCE)
12+
await cmd_scheduler.submit(
13+
task_id,
14+
cmd_args=["python", executor_script, f"--config_path={config_path}"]
15+
)
1216

1317
def cancel(task_id):
1418
return cmd_scheduler.cancel_task(task_id)

runtime/python-executor/datamate/wrappers/datamate_wrapper.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88

99
async def submit(task_id, config_path, retry_count: int = 0):
1010
current_dir = os.path.dirname(__file__)
11+
executor_script = os.path.join(current_dir, 'datamate_executor.py')
1112

1213
if not is_k8s():
13-
await cmd_scheduler.submit(task_id, f"python {os.path.join(current_dir, 'datamate_executor.py')} "
14-
f"--config_path={config_path}")
14+
# Use argument list to avoid shell injection (CodeQL / FCE)
15+
await cmd_scheduler.submit(
16+
task_id,
17+
cmd_args=["python", executor_script, f"--config_path={config_path}"]
18+
)
1519
return
1620

1721
script_path = os.path.join(current_dir, "datamate_executor.py")

0 commit comments

Comments
 (0)