From dc4379b235c856343f580bf3b3ac0e2d32c8660c Mon Sep 17 00:00:00 2001 From: ymq Date: Fri, 14 Aug 2026 14:20:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=9C=E7=A8=8B=E7=9B=AE=E5=BD=95=20?= =?UTF-8?q?remote=5Fdir=20=E6=94=B9=E7=94=A8=E7=9B=B8=E5=AF=B9=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=EF=BC=88=E4=BB=8E=E8=BF=9C=E7=A8=8B=E7=94=A8=E6=88=B7?= =?UTF-8?q?=20home=20=E7=9B=AE=E5=BD=95=E5=BC=80=E5=A7=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 _resolve_remote_dir(相对→~/xxx)+ _validate_remote_dir(防绝对路径/..穿越) - 移除 _FORBIDDEN_DIRS 系统目录黑名单(相对路径天然限制在 home 内) - rsync 迁移 + bwrap --bind 统一走 _resolve_remote_dir --- pipeline_service/work_env.py | 64 +++++++++++++++++------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/pipeline_service/work_env.py b/pipeline_service/work_env.py index b606bdc..b432bc3 100644 --- a/pipeline_service/work_env.py +++ b/pipeline_service/work_env.py @@ -132,12 +132,28 @@ def get_org_pubkey(org_id: str) -> dict: _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 的系统目录(防 rsync --delete 误删系统文件) -_FORBIDDEN_DIRS = { - "/", "/bin", "/sbin", "/usr", "/etc", "/lib", "/lib64", "/var", - "/root", "/boot", "/dev", "/proc", "/sys", "/tmp", "/opt", "/run", - "/srv", "/mnt", "/media", "/home", -} +# remote_dir 现在是相对路径(从远程登录用户的 home 目录开始)。 +# 相对路径天然限制在 home 内,无需系统目录黑名单,只需防路径穿越(..)。 + +def _resolve_remote_dir(remote_dir: str) -> str: + """把相对路径规范化为远程 home 下的路径(~/xxx)。""" + rel = (remote_dir or "").strip().strip("/") + return "~/" + rel if rel else "~" + + +def _validate_remote_dir(remote_dir: str) -> str: + """校验 remote_dir(相对路径),返回错误消息(None 表示通过)。""" + d = (remote_dir or "").strip() + if not d: + return "缺少 remote_dir" + if d.startswith("/"): + return "remote_dir 必须是相对路径(从远程用户 home 目录开始)" + parts = [p for p in d.split("/") if p] + if ".." in parts: + return "remote_dir 不能包含 .." + if not parts: + return "remote_dir 不能为空" + return None def _validate_remote(remote_config: dict) -> str: @@ -167,19 +183,9 @@ def _validate_remote(remote_config: dict) -> str: except (ValueError, TypeError): return "remote_port 必须是数字" - remote_dir = (remote_config.get("remote_dir") or "").strip() - if not remote_dir: - return "缺少 remote_dir" - if not remote_dir.startswith("/"): - return "remote_dir 必须是绝对路径" - _dir = remote_dir.rstrip("/") - if not _dir: - return "remote_dir 不能是根目录 /" - if _dir in _FORBIDDEN_DIRS: - return "remote_dir 不能是系统目录 " + _dir - for d in _FORBIDDEN_DIRS: - if d != "/" and _dir.startswith(d + "/"): - return "remote_dir 不能位于系统目录 " + d + " 下" + err = _validate_remote_dir(remote_config.get("remote_dir", "")) + if err: + return err key = (remote_config.get("remote_key_path") or "").strip() if key: @@ -214,17 +220,9 @@ async def migrate_work_dir(account_name: str, from_mode: str, to_mode: str, env: # remote_dir 安全校验(纵深防御:DB 脏数据/历史记录也拦一道) if from_mode == "remote" or to_mode == "remote": - remote_dir = (env.get("remote_dir") or "").strip() - if not remote_dir: - return {"ok": False, "error": "缺少远程目录 remote_dir"} - _d = remote_dir.rstrip("/") - if not _d: - return {"ok": False, "error": "远程目录是根目录,拒绝迁移"} - if _d in _FORBIDDEN_DIRS: - return {"ok": False, "error": "远程目录是系统目录,拒绝迁移: " + _d} - for d in _FORBIDDEN_DIRS: - if d != "/" and _d.startswith(d + "/"): - return {"ok": False, "error": "远程目录位于系统目录下,拒绝迁移: " + d} + err = _validate_remote_dir(env.get("remote_dir", "")) + if err: + return {"ok": False, "error": err} # 构造 rsync 命令 try: @@ -233,7 +231,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"} - target = _ssh_target(env) + ":" + remote_dir + target = _ssh_target(env) + ":" + _resolve_remote_dir(remote_dir) cmd = ["rsync", "-az", "--delete", "-e", "ssh " + " ".join(_ssh_common_args(env)), local_dir.rstrip("/") + "/", target.rstrip("/") + "/"] @@ -242,7 +240,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) + ":" + remote_dir + src = _ssh_target(env) + ":" + _resolve_remote_dir(remote_dir) cmd = ["rsync", "-az", "--delete", "-e", "ssh " + " ".join(_ssh_common_args(env)), src.rstrip("/") + "/", local_dir.rstrip("/") + "/"] @@ -366,7 +364,7 @@ def _remote_bwrap_cmd(env: dict, deploy_dir: str, command: str, workdir: str = " "--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", - "--bind", deploy_dir, "/home", + "--bind", _resolve_remote_dir(deploy_dir), "/home", "--chdir", "/home", "--setenv", "HOME", "/home", "--", "bash", "-c", command,