SuperAPI AI Directory

path-safety

Security & auditing

View repository

Updated 10-16 00:31

About this skill

Path Safety — Server-Side Secure Coding Standards

Source:docs/dev-guides/8-path-safety.md(formal development standards) + design-docs/issues/002-arbitrary-file-read-audit.md(security audit review). This document distills 6 coding standards that must be followed. Violating any one of them may introduce a path traversal (LFI) vulnerability.


R1. File download: use ConfinedDir.resolve() + send_file, do not use send_from_directory

Reason:send_from_directory(dir, user_input) internally performs a user_input second path resolution, creating a TOCTOU inconsistency with the preceding security check.

# ❌ BAD — 安全检查用 resolved target,发送用原始 filename,两次解析不一致
target = (scratch_dir / filename).resolve()
target.relative_to(scratch_dir.resolve())  # 检查通过
return send_from_directory(str(scratch_dir), filename)  # 再次解析
✅ GOOD — check and send use the same resolved path

scratch_jail = workspace.confined_scratch target = scratch_jail.resolve(filename) return send_file(target) # directly use the already-validated path


`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 tools reuse Workspace.confined_*

Reason: Agent tool parameters are generated by the LLM and must be treated as indirect user input. Do not hand-write Path(root) / rel_path or resolve() + relative_to()inside the tool; at the entry point reuse the ConfinedDir。

# ❌ 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. Prefer ConfinedDir, prohibit bare path concatenation

Reason:Path(root) / user_input is a high-frequency entry point for path traversal.ConfinedDir encapsulates three layers of defense (reject absolute paths → reject .. segments → resolve + is_relative_to)。

from data_formulator.security.path_safety import ConfinedDir

# ❌ BAD — 手动拼接 + 手动校验,容易遗漏
local_file = tmp_path / blob_relative_name
local_file.parent.mkdir(parents=True, exist_ok=True)
local_file.write_bytes(data)

# ✅ GOOD — ConfinedDir 自动校验 + 创建父目录
jail = ConfinedDir(tmp_path, mkdir=False)
jail.write(blob_relative_name, data)  # 自动校验 + 写入

Hierarchical relationship of existing secure APIs

用户输入(filename / relative_path / blob key)
    │
    ▼
safe_data_filename() / secure_filename()     ← 第一层:输入清洗
    │
    ▼
ConfinedDir.resolve()                        ← 第二层:路径约束
    │
    ▼
安全的 Path 对象

Scenarios that use Workspace.get_file_path() do not need to manually call ConfinedDir, because it already includes equivalent validation internally.


R5. Host filesystem access must have a deployment mode guard

Reason: Desktop single-user mode allows access to the local filesystem (expected behavior), but under multi-user/cloud deployment it is equivalent to opening server read permissions.

Standard: Any new Loader / Connector that involves directly reading the host filesystem (not through the Workspace API) must:

  1. In data_loader/__init__.py 's _enforce_deployment_restrictions() , register a disable rule
  2. Ensure create_connector() rejects disabled types
# 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"

Criterion: If the Loader's constructor accepts a user-controllable local path (such as root_dir), it needs a deployment guard.


R6. Multi-user deployment must enable the sandbox

Reason:not_a_sandbox mode, LLM-generated code executes directly in the host process and can bypass all path checks.

app.py already detects this configuration at startup and outputs a logger.critical warning. New sandbox modes or deployment scripts should ensure:

  • WORKSPACE_BACKEND != "local" when,SANDBOX must be docker or local
  • CI/CD deployment templates set by default SANDBOX=docker

Quick reference: security checklist when adding new code

Scenario What must be done
Add a new file download route Use ConfinedDir.resolve() to get the path, then send_file(resolved_path); do not use send_from_directory
Add a new Agent tool (read file/list directory) At the entry point reuse workspace.confined_root / workspace.confined_scratch, inside the tool only call jail.resolve()
Path containment check Use ConfinedDir.resolve(), do not hand-write Path.is_relative_to() or str.startswith()
Path(root) / variable mode Switch to ConfinedDir or Workspace.get_file_path()
Add local filesystem Loader In _enforce_deployment_restrictions() register multi-user disable
Deployment configuration Multi-user mode must SANDBOX=docker or SANDBOX=local

Reference documentation

  • docs/dev-guides/8-path-safety.md — Server-side path security development specification
  • design-docs/6-path-safety-confined-dir.md — Remaining unfinished implementation items status page
  • design-docs/issues/002-arbitrary-file-read-audit.md — Security audit review report
  • py-src/data_formulator/security/path_safety.py — ConfinedDir source code