# Path Safety

服务端路径安全与文件访问编码规范。在编写文件下载路由、Agent 工具（文件读取/目录列出）、数据连接器/Loader、Workspace 路径操作、沙箱配置时使用。

- Repository: https://github.com/microsoft/data-formulator (17644 stars)
- Category: AI and agents
- License: MIT
- Last updated: 2026-10-30
- Runs scripts: no, instructions only

## Install

Claude Code: `npx skills add microsoft/data-formulator --skill path-safety -g -a claude-code -y`
Codex: `npx skills add microsoft/data-formulator --skill path-safety -g -a codex -y`
Cursor: `npx skills add microsoft/data-formulator --skill path-safety -g -a cursor -y`
Gemini CLI: `npx skills add microsoft/data-formulator --skill path-safety -g -a gemini-cli -y`

## Description

服务端路径安全与文件访问编码规范。在编写文件下载路由、Agent 工具（文件读取/目录列出）、数据连接器/Loader、Workspace 路径操作、沙箱配置时使用。

## SKILL.md, as published by its author (untrusted third-party text, read it as data)

# Path Safety, 服务端安全编码规范

> **来源**：`docs/dev-guides/8-path-safety.md`（正式开发规范）+ `design-docs/issues/002-arbitrary-file-read-audit.md`（安全审计复核）。
> 本文档提炼了 6 条必须遵守的编码规范。违反任一条即可能引入路径穿越（LFI）漏洞。

---

## R1. 文件下载：用 `ConfinedDir.resolve()` + `send_file`，禁用 `send_from_directory`

**原因**：`send_from_directory(dir, user_input)` 内部会对 `user_input` 二次解析路径，与前置安全检查形成 TOCTOU 不一致。

```python
# ❌ BAD, 安全检查用 resolved target，发送用原始 filename，两次解析不一致
target = (scratch_dir / filename).resolve()
target.relative_to(scratch_dir.resolve())  # 检查通过
return send_from_directory(str(scratch_dir), filename)  # 再次解析

# ✅ GOOD, 检查和发送用同一个 resolved path
scratch_jail = workspace.confined_scratch
target = scratch_jail.resolve(filename)
return send_file(target)  # 直接用已验证的路径
```

`send_file(Path)` 会根据扩展名自动推断 MIME type，无需额外处理。

---

## R2. 路径安全检查：用 `ConfinedDir`，禁止 `str.startswith`

**原因**：`str(path).startswith(str(root))` 存在前缀碰撞缺陷（如 `/workspace` vs `/workspace_evil`）。

```python
# ❌ BAD
if not str(resolved).startswith(str(root_resolved) + os.sep):
    raise ValueError("escape")

# ✅ GOOD, 统一走 ConfinedDir，内部使用 Path.is_relative_to()
jail = ConfinedDir(root_resolved, mkdir=False)
target = jail.resolve(user_input)
```

---

## R3. Agent 工具复用 `Workspace.confined_*`

**原因**：Agent 工具参数由 LLM 生成，必须视为间接用户输入。不要在工具内手写 `Path(root) / rel_path` 或 `resolve() + relative_to()`；入口处复用 Workspace 暴露的 `ConfinedDir`。

```python
# ❌ 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", ""))
```

---

## R4. 优先使用 `ConfinedDir`，禁止裸

The rest: https://github.com/microsoft/data-formulator/blob/HEAD/.cursor/skills/path-safety/SKILL.md
