
Data Formulator 服务端代码如何统一使用 ConfinedDir 防止路径穿越【免费下载链接】data-formulator Data Formulator is an interactive AI-powered data analysis system makes it easy to connect, explore and visualize data.项目地址: https://gitcode.com/GitHub_Trending/da/data-formulator在 Data Formulator 中新增一个后端模块——不管是接收 HTTP 路径参数的 route、LLM 生成参数的 Agent 工具还是访问 Workspace 文件的新 Loader——只要代码会把外部传入的路径片段拼到服务端目录上就可能引入路径穿越漏洞。项目已经为此定下了统一做法所有路径约束都走 ConfinedDir 这一个原语禁止手写resolve() relative_to()检查。开发规范全文见 docs/dev-guides/8-path-safety.md本文按实际接入顺序讲解它怎么用、在哪里用、以及怎么写测试验证。动手前ConfinedDir 提供了什么ConfinedDir是一个把任意路径操作限制在根目录内的 jail位于 py-src/data_formulator/security/path_safety.py导入方式from data_formulator.security.path_safety import ConfinedDir jail ConfinedDir(tmp_path, mkdirFalse) # mkdirFalse 表示不自动创建目录 target jail.resolve(data/report.csv) # 逃出 root 时抛 ValueError jail.write(scratch/output.csv, bcontent) # resolve 建父目录 写字节构造函数签名是ConfinedDir(root, *, mkdir: bool True)默认会创建根目录。核心方法resolve()分四层校验见 path_safety.py 第 49–80 行拒绝空路径拒绝绝对路径拒绝..路径段拼接到 root 后用Path.resolve()展开符号链接再用Path.is_relative_to()确认结果仍在 root 内——这一层专门拦截 symlink escape。任何一层失败都抛出ValueError。调用方的处理约定捕获ValueError时返回应用层错误或跳过该不可信对象不要继续用原始路径。除resolve()外类还提供了一组同样经过校验的扩展 API按需选用API行为write(relative, data)resolve 建父目录 写字节read_text/write_text读写文本write_text自动建父目录exists(relative)逃出 jail 的路径返回False而不是抛异常方便调用方把穿越当 not found 处理iterdir/rglob在 jail 内列目录、递归 globunlink(relative)删除 jail 内文件jail / sub/path运算符重载等价于jail.resolve(sub/path)实例构造后不可变__slots__只存_root文档中注明了它是线程安全的可以在多线程场景复用同一实例。按场景选择路径约束入口开发规范给了一张场景到 API 的映射表写新代码时先对照它选入口而不是自己拼Path场景规范用法Workspace data 文件Workspace.get_file_path()内部已用ConfinedDirWorkspace 根/data/scratch 子目录workspace.confined_root/confined_data/confined_scratch属性Agent 读文件/列目录工具workspace.confined_root/workspace.confined_scratch传入各工具方法文件下载 routeworkspace.confined_scratch.resolve(filename)后传给send_file()文件上传 routesecure_filename()清洗 workspace.confined_scratch.resolve()二次校验任意 root relative pathConfinedDir(root).resolve(relative)知识库文件读写通过KnowledgeStore内部的ConfinedDir推理日志写入通过ReasoningLogger内部的ConfinedDir读取宿主文件系统的 Loader必须注册多用户部署禁用规则Sandbox 部署多用户模式不得使用not_a_sandbox两条硬性禁令贯穿所有场景禁止手写resolve() relative_to()/is_relative_to()检查这些逻辑统一封装在ConfinedDir.resolve()中禁止把用户、LLM、外部存储、HTTP body/query/path 参数直接裸拼接例如Path(root) / user_input。文件下载 route检查与发送必须用同一条路径下载接口的要求是安全检查和实际发送文件使用同一个 resolved path否则检查与发送之间可能形成 TOCTOU 不一致。规范中的标准写法from flask import send_file scratch_jail workspace.confined_scratch try: target scratch_jail.resolve(filename) except ValueError: return jsonify(statuserror, messageAccess denied) return send_file(target)对应的真实实现在 routes/agents.py 的scratch_serveresolve()抛ValueError时抛ACCESS_DENIED错误文件不存在时抛TABLE_NOT_FOUND最后send_file(target)发送的就是 resolve 之后的路径。规范同时明确两个禁止项不要在用户路径上使用send_from_directory(dir, filename)——Flask 内部会再次解析原始filename容易和前置检查不一致文件名来自用户输入并写入Content-Disposition时不得直接插值原始字符串应先建统一 helper 去除 CR/LF、引号和目录组件。文件上传 route清洗与约束是两层上传侧要同时做两件事先用secure_filename()清洗文件名再用ConfinedDir.resolve()二次校验。scratch_upload展示了完整链路读取请求文件后safe_name _werkzeug_secure_filename(file.filename) base, ext os.path.splitext(safe_name) final_name f{base}_{file_hash}{ext} # 附加内容哈希后缀 try: dest scratch_jail.resolve(final_name) except ValueError: raise AppError(ErrorCode.VALIDATION_ERROR, Invalid filename) dest.write_bytes(raw)secure_filename负责把文件名收敛到 ASCII 安全范围identity、URL/上传临时文件名等场景用它而resolve()负责保证清洗后的相对路径不会逃出 root。Agent 工具入口创建一次 jail工具方法复用Agent 工具参数由 LLM 生成、LLM 输入又来自用户因此路径参数一律视为不可信。接入模式分两步。第一步在工具入口函数拿到ConfinedDir实例所有工具方法共用def _execute_tool(self, name, args): workspace_jail self.workspace.confined_root scratch_jail self.workspace.confined_scratch if name read_file: return self._tool_read_file(args, workspace_jail) elif name write_file: return self._tool_write_file(args, scratch_jail) ...第二步工具方法内用resolve()拿安全路径失败直接返回错误而不是重试原始路径def _tool_read_file(self, args, workspace_jail): rel_path args.get(path, ) try: target workspace_jail.resolve(rel_path) except ValueError: return {error: Access denied: path outside workspace} ...注意规范特意强调不要在每个工具函数内部反复创建ConfinedDir或反复调用resolve()在_execute_tool入口创建一次即可。workspace.confined_root/confined_scratch这类属性返回的是Workspace在构造时就建好的实例见下文工具层不需要再建。Workspace 与既有模块复用现成实例Workspace.init在初始化时就创建并暴露了三个 jail是所有调用方agents、routes的单一事实来源self._confined_root ConfinedDir(self._path, mkdirFalse) self._confined_data ConfinedDir(self._path / data) self._confined_scratch ConfinedDir(self._path / scratch)对外通过confined_root、confined_data、confined_scratch三个只读属性暴露data/和scratch/会在构造时自动创建。此外 Workspace 构造阶段还有一道自检legacy 模式下拼出的工作区路径会被ConfinedDir(self._root).resolve(self._safe_id)校验逃出 root 时直接抛出Path traversal detected错误。Workspace.get_file_path(filename)是访问 data 文件的推荐入口内部是两层防御workspace.py 第 362–366 行basename safe_data_filename(filename) try: return self._confined_data.resolve(basename) except ValueError: raise ValueError(fPath traversal detected: {filename!r})第一层safe_data_filename()保留 Unicode、去掉目录组件和控制字符第二层ConfinedDir.resolve()校验结果不逃出data/。所以使用get_file_path()的场景不需要手动再调ConfinedDir。知识库和推理日志这两个模块是任意 root relative path场景的参考实现KnowledgeStore 用ConfinedDir(user_home / knowledge, mkdirTrue)建立总根再按rules/workflows等分类各建一个子 jailCRUD 全部经过对应 jailReasoningLogger 在按天切换日志时构造ConfinedDir(agent_logs_root / today / safe_identity_id, mkdirTrue)日志文件路径用jail.resolve(self._filename, mkdir_parentsTrue)解析。读取宿主文件系统的 Loader 与 Sandbox 边界有两类代码不属于接 ConfinedDir而是有独立的部署级约束宿主文件系统 Loader如果 Loader 构造参数包含用户可控的本机路径如root_dir它只能在本地单用户模式使用必须在 data_loader/__init__.py 的_enforce_deployment_restrictions()中注册禁用规则——当WORKSPACE_BACKEND不是local时从DATA_LOADERS删除该 Loader 并给出禁用说明。local_folder是当前的参考实现它也是ConfinedDir的原始采用者。Sandbox 部署not_a_sandbox会让 LLM 生成的 Python 代码在宿主进程直接执行可能绕过所有路径检查。规范要求WORKSPACE_BACKEND local时允许桌面单用户使用not_a_sandbox非local后端必须使用SANDBOXdocker或SANDBOXlocal。应用启动时的安全检查只是输出 critical 日志告警而非硬阻断所以生产部署还必须在部署配置或启动脚本层强制隔离沙箱。用测试验证接入是否正确规范对新增路径相关代码的测试要求是至少覆盖正常相对路径可访问../、绝对路径、空路径被拒绝symlink escape 被拒绝适用时用真实文件系统测下载 route 使用send_file(resolved_path)多用户部署下宿主文件系统 Loader 被禁用。仓库里已有一组可直接参考的测试覆盖了 ConfinedDir 的扩展 API 和 Phase 5 迁移后的行为tests/backend/security/test_confined_dir_extended.py——read_text/write_text/exists/iterdir/rglob/unlink的正常路径与穿越拒绝tests/backend/security/test_confined_dir_migration.py——Workspace.confined_*属性、Agent 工具与 scratch route 的回归测试tests/backend/agents/test_tool_path_safety.py、tests/backend/security/test_scratch_serve.py、tests/backend/data/test_local_folder_loader.py、tests/backend/security/test_local_folder_deployment.py、tests/backend/security/test_startup_safety.py。测试断言的形态可以照抄例如 test_confined_dir_migration.py 中测试代码非运行时输出def test_confined_root_rejects_traversal(self, workspace): with pytest.raises(ValueError): workspace.confined_root.resolve(../../etc/passwd) def test_get_file_path_traversal_sanitized(self, workspace): # 第一层 safe_data_filename 去掉目录组件../../etc/passwd 变成 passwd # 第二层 ConfinedDir 保证结果仍落在 data/ 内 path workspace.get_file_path(../../etc/passwd) assert path.parent workspace.confined_data.root运行方式与仓库其余后端测试一致pytest.ini 已配置testpaths tests/backend tests/frontend和backend/security等 markerspython -m pytest tests/backend/security/test_confined_dir_extended.py \ tests/backend/security/test_confined_dir_migration.py新模块合入前用规范第 9 节的 checklist 自查一遍最省事新模块是否接收外部路径片段是否用了ConfinedDir或Workspace.get_file_path()是否避免了裸拼接和手写 resolve 检查下载 route 是否resolve()后传给send_file()上传 route 是否同时用了secure_filename()和resolve()宿主 Loader 是否注册了多用户禁用规则多用户部署是否启用了docker/localsandbox规范末尾的已迁移清单datalake/workspace.py、agent_data_loading_chat.py、routes/agents.py、knowledge/store.py、agents/reasoning_log.py等位置列出了每个存量模块的迁移方式新代码与这些实现保持一致就是符合统一约束的做法。【免费下载链接】data-formulator Data Formulator is an interactive AI-powered data analysis system makes it easy to connect, explore and visualize data.项目地址: https://gitcode.com/GitHub_Trending/da/data-formulator创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考