
Data Formulator 服务端路径安全编码规范基于 ConfinedDir 的统一路径约束与 LFI 防御实战【免费下载链接】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 仓库的.cursor/skills/path-safety/SKILL.md及正式开发规范 docs/dev-guides/8-path-safety.md为骨架系统讲解服务端如何用ConfinedDir统一约束不可信路径片段 服务端根目录的组合杜绝路径穿越LFI漏洞。读者将掌握文件下载路由、Agent 工具、Data Loader、沙箱部署四类场景的安全编码范式并理解为什么send_from_directory、str.startswith、手写resolve() relative_to()是必须淘汰的反模式。一、背景与核心原则Data Formulator 是一个交互式 AI 数据分析系统后端大量存在把用户、LLM、外部存储或 HTTP 参数提供的路径片段拼接到服务端根目录的代码路径。SKILL.md 开篇即给出总纲任何把不可信路径片段拼到服务端根目录上的代码都必须先经过统一路径约束。按威胁来源划分不可信路径输入主要来自四类输入来源典型场景用户 HTTP 参数文件下载/上传 route 的filename、query 参数LLM 生成参数Agent 工具调用中的path、directory参数外部存储Blob key、Catalog 元数据中的相对路径插件/知识库知识文件读写、推理日志写入路径该规范的适用范围覆盖后端 route、Agent 工具、Workspace 文件访问、Data Loader、Sandbox、插件文件 I/O、知识库与推理日志见 docs/dev-guides/8-path-safety.md。规范同时给出两条硬性红线绝对禁止手写resolve() relative_to()或resolve() is_relative_to()路径检查模式——这些逻辑已统一封装在ConfinedDir.resolve()中手写会导致逻辑重复、不一致和遗漏禁止把用户、LLM、外部存储、HTTP body/query/path 参数直接用于裸路径拼接例如Path(root) / user_input、root / filename。这两条红线是全文 6 条编码规范R1R6的共同前提。二、R1文件下载——用ConfinedDir.resolve()send_file禁用send_from_directory反模式与缺陷SKILL.md 明确指出send_from_directory(dir, user_input)内部会对user_input二次解析路径与前置安全检查形成 TOCTOUTime-of-Check to Time-of-Use不一致# ❌ BAD — 安全检查用 resolved target发送用原始 filename两次解析不一致 target (scratch_dir / filename).resolve() target.relative_to(scratch_dir.resolve()) # 检查通过 return send_from_directory(str(scratch_dir), filename) # 再次解析即使前置检查通过send_from_directory在 Flask 内部会再次解析原始filename此时若文件名中夹带符号链接或特殊路径片段实际发送的文件可能与安全检查的对象不一致从而绕过防护。正确范式# ✅ GOOD — 检查和发送用同一个 resolved path scratch_jail workspace.confined_scratch target scratch_jail.resolve(filename) return send_file(target) # 直接用已验证的路径send_file(Path)会根据扩展名自动推断 MIME type无需额外处理。这正是 routes/agents.py 中scratch_serve采用的方式先经workspace.confined_scratch.resolve(filename)校验再交给send_file。捕获ValueError时应返回应用层错误如jsonify(statuserror, messageAccess denied)不要继续使用原始路径。关于 Content-Disposition 的附加要求如果文件名来自用户输入并写入Content-Disposition不得直接插值原始字符串。新代码应先建立统一 helper去除 CR/LF、引号和目录组件并为非 ASCII 名称提供安全 fallback 或filename*编码这是文件名清洗层的职责见第四节。三、R2路径安全检查——用ConfinedDir禁止str.startswith前缀碰撞缺陷str(path).startswith(str(root))存在前缀碰撞缺陷/workspace会误认为/workspace_evil是它的子路径因为后者也以/workspace开头。# ❌ BAD if not str(resolved).startswith(str(root_resolved) os.sep): raise ValueError(escape) # ✅ GOOD — 统一走 ConfinedDir内部使用 Path.is_relative_to() jail ConfinedDir(root_resolved, mkdirFalse) target jail.resolve(user_input)ConfinedDir内部使用的是Path.is_relative_to()它按路径段而非字符串前缀判断包含关系天然免疫前缀碰撞。四、R3Agent 工具复用Workspace.confined_*不手写校验为什么 Agent 工具是重灾区Agent 工具参数由 LLM 生成而 LLM 输入又来自用户因此路径参数一律视为不可信。如果每个工具函数内各自手写路径拼接与校验很容易出现遗漏或行为不一致。正确范式入口集中校验工具只做 resolve# ❌ BAD — 手写路径拼接和校验 def _tool_read_file(self, args, workspace_path): target (workspace_path / rel_path).resolve() target.relative_to(workspace_path) # ✅ GOOD — 入口拿到 ConfinedDir工具只调用 jail.resolve() def _execute_tool(self, name, args): workspace_jail self.workspace.confined_root scratch_jail self.workspace.confined_scratch return self._tool_read_file(args, workspace_jail) def _tool_read_file(self, args, workspace_jail): target workspace_jail.resolve(args.get(path, ))规范的附加约束不要在工具函数内部反复创建ConfinedDir或反复调用resolve()。应在_execute_tool入口创建一次所有工具方法复用同一个实例。从仓库源码结构看agent_data_loading_chat.py 是这项规范的参考实现_execute_tool获取workspace.confined_root与workspace.confined_scratch随后_tool_read_file、_tool_list_directory使用workspace_jail.resolve(rel_path)_tool_write_file使用scratch_jail.resolve(filename)_tool_execute_python则通过workspace.confined_scratch.resolve(safe_name .csv)约束临时文件位置。五、R4优先使用ConfinedDir禁止裸路径拼接三层防御ConfinedDir将路径约束封装为单一 choke point见 py-src/data_formulator/security/path_safety.py 中ConfinedDir.resolve()的实现其防护层次为拒绝空路径拒绝绝对路径含 Windows 盘符形式rel.root拒绝包含..路径段resolve()展开符号链接后用Path.is_relative_to()确认结果仍在 root 内——这一步捕获 symlink escape。from data_formulator.security.path_safety import ConfinedDir # ❌ BAD — 手动拼接 手动校验容易遗漏 local_file tmp_path / blob_relative_name local_file.parent.mkdir(parentsTrue, exist_okTrue) local_file.write_bytes(data) # ✅ GOOD — ConfinedDir 自动校验 创建父目录 jail ConfinedDir(tmp_path, mkdirFalse) jail.write(blob_relative_name, data) # 自动校验 写入ConfinedDir.write()内部先resolve(relative, mkdir_parentsTrue)再原子写入自动完成校验 创建父目录 写入三件事。已有安全 API 的层次关系用户输入filename / relative_path / blob key │ ▼ safe_data_filename() / secure_filename() ← 第一层输入清洗 │ ▼ ConfinedDir.resolve() ← 第二层路径约束 │ ▼ 安全的 Path 对象路径约束与文件名清洗是两层不同防线API用途safe_data_filename()Workspace 数据文件名保留 Unicode去掉目录组件和控制字符secure_filename()identity、URL/上传临时文件名等需要 ASCII 安全名的场景ConfinedDir.resolve()校验清洗后的相对路径不会逃出 root使用Workspace.get_file_path(filename)的场景不需要手动调用ConfinedDir因为它内部已通过safe_data_filename()ConfinedDir.resolve()实现两层防御。ConfinedDir还提供read_text、write_text、exists、iterdir、rglob、unlink等扩展 API以及运算符重载jail / sub/path等价于jail.resolve(...)方便目录列举与文件删除场景统一走约束。六、R5宿主文件系统访问必须设部署模式守卫问题本质桌面单用户模式允许访问本机文件系统预期行为但多用户/云部署下等于开放服务器读权限。因此任何新的 Loader / Connector 如果涉及直接读取宿主文件系统不通过 Workspace API必须注册多用户禁用规则。判断标准如果 Loader 的构造函数接受一个用户可控的本机路径如root_dir它就需要部署守卫。注册禁用规则# data_loader/__init__.py — 参考 local_folder 的处理方式 def _enforce_deployment_restrictions(): backend os.environ.get(WORKSPACE_BACKEND, local) if backend ! local: for key in (local_folder, your_new_local_loader): if key in DATA_LOADERS: del DATA_LOADERS[key] DISABLED_LOADERS[key] f{key} disabled in multi-user mode仓库中的真实实现位于 py-src/data_formulator/data_loader/init.py_enforce_deployment_restrictions()读取WORKSPACE_BACKEND环境变量默认local当后端不是local时从DATA_LOADERS中移除local_folder并写入DISABLED_LOADERS提示信息同时记录日志。该函数在模块加载时即被调用_enforce_deployment_restrictions()位于文件末尾因此多用户模式下local_folder连接器在启动阶段就被禁用UI 侧也能从DISABLED_LOADERS获取人类可读的禁用原因。此外create_connector()需要能拒绝已禁用的类型local_folder_data_loader.py 是当前仓库中该模式的参考实现文档明确标注其为ConfinedDir的原始采用者。通用 Data Loader 开发规范可进一步参考 docs/dev-guides/3-data-loader-development.md。七、R6多用户部署必须启用沙箱威胁模型not_a_sandbox模式下LLM 生成的 Python 代码在宿主进程直接执行可绕过所有路径检查——即便路径层防护再完善恶意代码依然可以访问任意文件。部署要求WORKSPACE_BACKEND local时允许桌面单用户模式使用not_a_sandboxWORKSPACE_BACKEND ! local时SANDBOX必须为docker或localCI/CD 部署模板中应默认设置SANDBOXdocker。app.py已在启动时检测此配置并输出logger.critical警告但当前实现是告警而非硬阻断——因此生产部署还必须在部署配置或启动脚本层强制SANDBOXlocal/SANDBOXdocker不能只依赖应用内告警。新增的沙箱模式或部署脚本应确保非本地模式下不允许not_a_sandbox出现在生效配置中。八、速查新增代码时的安全检查清单场景必须做的事新增文件下载路由用ConfinedDir.resolve()得到路径再send_file(resolved_path)不用send_from_directory新增 Agent 工具读文件/列目录入口复用workspace.confined_root/workspace.confined_scratch工具内只调用jail.resolve()路径包含判断用ConfinedDir.resolve()不要手写Path.is_relative_to()或str.startswith()Path(root) / variable模式改用ConfinedDir或Workspace.get_file_path()新增本机文件系统 Loader在_enforce_deployment_restrictions()中注册多用户禁用部署配置多用户模式必须SANDBOXdocker或SANDBOXlocalSKILL.md 同时保留了完整的 New Module Checklist见 docs/dev-guides/8-path-safety.md新模块开发时可逐项自检是否接收不可信路径片段、是否使用ConfinedDir、是否避免裸拼接、下载/上传 route 是否双层校验、Loader 是否注册禁用规则、沙箱是否启用。九、落地佐证规范在仓库中的实际迁移规范并非停留在纸面。docs/dev-guides/8-path-safety.md 第 10 节记录了已从手写resolve() relative_to()迁移到ConfinedDir的位置清单摘录如下文件方法迁移方式datalake/workspace.py__init__创建_confined_root/_confined_data/_confined_scratch暴露属性datalake/workspace.pyget_file_pathself._confined_data.resolve(basename)agents/agent_data_loading_chat.py_execute_toolworkspace.confined_rootworkspace.confined_scratchroutes/agents.pyscratch_serveworkspace.confined_scratch.resolve(filename)routes/agents.pyscratch_uploadworkspace.confined_scratch.resolve(final_name)knowledge/store.pyCRUDConfinedDir(user_home / knowledge / category)agents/reasoning_log.pylogConfinedDir(DATA_FORMULATOR_HOME / agent-logs / date / safe_identity_id)data_loader/local_folder_data_loader.py全文件已使用ConfinedDir原始采用者可以看到Workspace 的根目录、data、scratch 三个区域分别维护独立的ConfinedDir实例知识库文件读写与推理日志写入也各自通过内部ConfinedDir约束——整个项目形成了一次封装、处处复用的路径安全架构。测试保障规范第 8 节对路径相关代码提出了明确测试要求至少覆盖正常相对路径可访问../、绝对路径、空路径被拒绝symlink escape 被拒绝适用时用真实文件系统测试下载 route 使用send_file(resolved_path)多用户部署下宿主文件系统 Loader 被禁用。仓库中可对照验证的测试包括tests/backend/security/test_confined_dir_extended.py —ConfinedDir扩展行为测试tests/backend/agents/test_tool_path_safety.py — Agent 工具路径安全测试tests/backend/security/test_scratch_serve.py — scratch 文件服务下载安全测试tests/backend/security/test_local_folder_deployment.py — 多用户部署禁用测试tests/backend/security/test_startup_safety.py — 启动期安全检测测试tests/backend/data/test_local_folder_loader.py — local_folder Loader 测试十、总结与自查Data Formulator 的路径安全模型可以浓缩为一句话所有不可信路径进入文件系统前必须经过唯一的ConfinedDirchoke point。在此基础上结合.cursor/rules/path-safety.mdcpath-safety.mdc中列出的禁止/正确模式提交代码前请按以下清单自查✅ 新代码是否接收用户、LLM、外部存储或 HTTP 传入的路径片段✅ 是否使用了ConfinedDir或Workspace.get_file_path()✅ 是否避免了Path(root) / user_input裸拼接✅ 是否避免了手写resolve() relative_to()/is_relative_to()模式必须用ConfinedDir✅ 下载 route 是否用ConfinedDir.resolve()做检查并传给send_file()✅ 上传 route 是否同时使用secure_filename()和ConfinedDir.resolve()✅ 读取宿主文件系统的 Loader 是否注册了多用户禁用规则✅ 多用户部署是否启用了docker或localsandbox说明SKILL.md 原文引用的design-docs/6-path-safety-confined-dir.md与design-docs/issues/002-arbitrary-file-read-audit.md在当前仓库快照中不存在相关内容请以 docs/dev-guides/8-path-safety.md 与 py-src/data_formulator/security/path_safety.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),仅供参考