From 70185771d0f5652a0d35481f8880f3a05a080c5e Mon Sep 17 00:00:00 2001 From: ymq Date: Fri, 14 Aug 2026 14:26:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9C=E7=A8=8B=E7=9B=AE=E5=BD=95=20~?= =?UTF-8?q?=20=E5=B1=95=E5=BC=80=E6=94=B9=E7=94=A8=20$HOME=EF=BC=88shlex.q?= =?UTF-8?q?uote=20=E4=BC=9A=E6=8A=8A=20~=20=E5=8C=85=E8=BF=9B=E5=8D=95?= =?UTF-8?q?=E5=BC=95=E5=8F=B7=E4=B8=8D=E5=B1=95=E5=BC=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _resolve_remote_dir → _rel_remote_dir 返回纯相对路径 - rsync 用 :~/,ssh mkdir/bwrap --bind 用 $HOME/rel 让远程 shell 展开 - 加强 remote_dir 安全字符校验(_REL_DIR_RE,防 shell 注入) --- pipeline_service/work_env.py | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/pipeline_service/work_env.py b/pipeline_service/work_env.py index 70d2b52..70fe9f4 100644 --- a/pipeline_service/work_env.py +++ b/pipeline_service/work_env.py @@ -133,12 +133,14 @@ _HOST_RE = re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$") _USER_RE = re.compile(r"^[a-zA-Z0-9._-]+$") # remote_dir 现在是相对路径(从远程登录用户的 home 目录开始)。 -# 相对路径天然限制在 home 内,无需系统目录黑名单,只需防路径穿越(..)。 +# 相对路径天然限制在 home 内,无需系统目录黑名单,只需防路径穿越(..)与 shell 注入。 -def _resolve_remote_dir(remote_dir: str) -> str: - """把相对路径规范化为远程 home 下的路径(~/xxx)。""" - rel = (remote_dir or "").strip().strip("/") - return "~/" + rel if rel else "~" +_REL_DIR_RE = re.compile(r"^[a-zA-Z0-9._/-]+$") + + +def _rel_remote_dir(remote_dir: str) -> str: + """规范化相对路径(strip 前后 /),返回纯相对路径(不含 ~,避免 shlex.quote 后 ~ 不展开)。""" + return (remote_dir or "").strip().strip("/") def _validate_remote_dir(remote_dir: str) -> str: @@ -153,6 +155,8 @@ def _validate_remote_dir(remote_dir: str) -> str: return "remote_dir 不能包含 .." if not parts: return "remote_dir 不能为空" + if not _REL_DIR_RE.match(d): + return "remote_dir 只能含字母数字和 . _ - / 字符" return None @@ -205,10 +209,14 @@ def _validate_remote(remote_config: dict) -> str: # ═══════════════════════════════════════════════════════════ async def _remote_mkdir(env: dict, remote_dir: str) -> bool: - """远程 mkdir -p(相对路径可能多级,rsync 不会递归建父目录)。失败返回 False。""" + """远程 mkdir -p(相对路径可能多级,rsync 不会递归建父目录)。失败返回 False。 + + remote_dir 已校验为安全字符(_REL_DIR_RE),用 $HOME 拼接让远程 shell 展开 + (不用 ~ 前缀,因 shlex.quote 会把 ~ 包进单引号导致不展开)。 + """ try: target = _ssh_target(env) - ssh_cmd = ["ssh"] + _ssh_common_args(env) + [target, "mkdir -p " + shlex.quote(remote_dir)] + ssh_cmd = ["ssh"] + _ssh_common_args(env) + [target, 'mkdir -p "$HOME/' + remote_dir + '"'] proc = await asyncio.create_subprocess_exec( *ssh_cmd, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL) await asyncio.wait_for(proc.communicate(), timeout=30) @@ -244,8 +252,8 @@ async def migrate_work_dir(account_name: str, from_mode: str, to_mode: str, env: remote_dir = env.get("remote_dir", "") if not remote_dir: return {"ok": False, "error": "缺少远程目录 remote_dir"} - await _remote_mkdir(env, _resolve_remote_dir(remote_dir)) - target = _ssh_target(env) + ":" + _resolve_remote_dir(remote_dir) + await _remote_mkdir(env, _rel_remote_dir(remote_dir)) + target = _ssh_target(env) + ":~/" + _rel_remote_dir(remote_dir) cmd = ["rsync", "-az", "--delete", "-e", "ssh " + " ".join(_ssh_common_args(env)), local_dir.rstrip("/") + "/", target.rstrip("/") + "/"] @@ -254,7 +262,7 @@ async def migrate_work_dir(account_name: str, from_mode: str, to_mode: str, env: remote_dir = env.get("remote_dir", "") if not remote_dir: return {"ok": False, "error": "缺少远程目录 remote_dir"} - src = _ssh_target(env) + ":" + _resolve_remote_dir(remote_dir) + src = _ssh_target(env) + ":~/" + _rel_remote_dir(remote_dir) cmd = ["rsync", "-az", "--delete", "-e", "ssh " + " ".join(_ssh_common_args(env)), src.rstrip("/") + "/", local_dir.rstrip("/") + "/"] @@ -364,9 +372,9 @@ def _row_to_env_from_dict(data: dict) -> dict: # 远程沙箱执行 # ═══════════════════════════════════════════════════════════ -def _remote_bwrap_cmd(env: dict, deploy_dir: str, command: str, workdir: str = "") -> list: - """构造远程主机的 bwrap 沙箱参数(不含 bwrap 前缀,由调用方拼接探测逻辑)。""" - parts = [ +def _bwrap_static_args() -> list: + """bwrap 沙箱固定参数(不含 --bind 目录和 command,由调用方拼接)。""" + return [ "--unshare-user", "--unshare-pid", "--unshare-ipc", "--unshare-uts", "--die-with-parent", "--ro-bind", "/usr", "/usr", @@ -378,12 +386,7 @@ def _remote_bwrap_cmd(env: dict, deploy_dir: str, command: str, workdir: str = " "--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", - "--bind", _resolve_remote_dir(deploy_dir), "/home", - "--chdir", "/home", - "--setenv", "HOME", "/home", - "--", "bash", "-c", command, ] - return parts async def run_remote_sandbox(env: dict, deploy_dir: str, command: str, @@ -392,10 +395,13 @@ async def run_remote_sandbox(env: dict, deploy_dir: str, command: str, ssh_args = _ssh_common_args(env) target = _ssh_target(env) - bwrap_parts = _remote_bwrap_cmd(env, deploy_dir, command, workdir) - bwrap_args = " ".join(shlex.quote(p) for p in bwrap_parts) - # 远程 shell 内探测 bwrap 路径(PATH 优先,回退 $HOME/bin/bwrap) - inner = 'BW=$(command -v bwrap 2>/dev/null || echo "$HOME/bin/bwrap"); exec "$BW" ' + bwrap_args + # deploy_dir 是相对路径(已校验安全字符),用 $HOME 拼接让远程 shell 展开 + rel = _rel_remote_dir(deploy_dir) + inner = ('BW=$(command -v bwrap 2>/dev/null || echo "$HOME/bin/bwrap"); ' + 'RD="$HOME/' + rel + '"; ' + 'exec "$BW" ' + " ".join(_bwrap_static_args()) + ' ' + '--bind "$RD" /home --chdir /home --setenv HOME /home ' + '-- bash -c ' + shlex.quote(command)) ssh_cmd = ["ssh"] + ssh_args + [target, inner]