> For the complete documentation index, see [llms.txt](https://yeasy.gitbook.io/harness_engineering_guide/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yeasy.gitbook.io/harness_engineering_guide/di-si-bu-fen-an-quan-ping-gu-yu-yan-jin/12_security/12.4_path_validation.md).

# 12.4 路径校验与注入防护

路径校验是Harness安全中 **最复杂且最关键** 的环节。文件系统是Agent最常访问的资源，路径穿越攻击也是最易发动的攻击。一个强大的路径校验器需要应对多个层次的编码和规范化攻击。

本节从路径穿越的常见攻击向量入手，介绍Claude Code的5层递进式防护机制，并通过实际案例展示其如何应对各种攻击。

## 12.4.1 路径穿越攻击向量

### 向量1：相对路径穿越

相对路径穿越的攻击示例：

```
工作目录： /tmp/agent/
请求： ../../../etc/passwd
规范化： /etc/passwd
结果： 逃逸成功
```

### 向量2：URL编码穿越

URL编码穿越的攻击示例：

```
请求： ..%2f..%2fetc%2fpasswd
解码： ../../etc/passwd
规范化： /etc/passwd
结果： 逃逸成功
```

### 向量3：双重URL编码穿越

攻击示例如下：

```
请求： ..%252f..%252fetc%252fpasswd
首次解码： ..%2f..%2fetc%2fpasswd
验证器可能只解码一次,再调用可能二次解码
结果： 逃逸成功
```

### 向量4：Unicode标准化攻击

具体示例如下：

```
U+2044 (FRACTION SLASH,看起来像/)
"etc\u2044passwd" 规范化为 "etc/passwd"

使用不同的Unicode形式：
- NFD (Decomposed): é = e + combining acute
- NFC (Composed): é
验证后改变规范化方式可能导致绕过
```

### 向量5：反斜杠注入

攻击模式如下：

```
工作目录： C:\agents\work\
请求： ..\..\windows\system32
验证器如果只检查/, 会遗漏\
结果： 逃逸成功(Windows系统)
```

### 向量6：符号链接逃逸

具体场景如下：

```
/tmp/agent/link -> /etc/passwd (符号链接)
验证： 链接在工作目录内 ✓
访问： readlink /tmp/agent/link -> /etc/passwd
结果： 逃逸成功
```

### 向量7：Case Sensitivity/Insensitivity

案例如下：

```
验证黑名单： "/etc/passwd" 禁止
请求： "/ETC/PASSWD" (某些文件系统不区分大小写)
结果： 逃逸成功
```

## 12.4.2 Claude Code 的 pathValidation.ts - 5层防护

Claude Code实现了5层递进式防护，每一层都针对特定的攻击向量：

```python
import os
import re
from pathlib import Path
from urllib.parse import unquote
import unicodedata

class PathValidator:
    """
    5层递进式路径校验(基于Claude Code pathValidation.ts)
    """

    def __init__(self, base_path: str = "/tmp/agent"):
        self.base_path = os.path.abspath(base_path)
        self.max_path_length = 4096

    def validate(self, user_path: str) -> str:
        """
        完整的路径校验流程
        返回规范化后的安全路径,或抛异常
        """

        # 第一层：长度检查
        self._check_length(user_path)

        # 第二层：URL解码(全部解码,迭代检查)
        decoded_path = self._decode_all_encodings(user_path)

        # 第三层：Unicode规范化
        normalized_path = self._normalize_unicode(decoded_path)

        # 第四层：平台特定规范化(处理反斜杠)
        platform_normalized = self._normalize_platform(normalized_path)

        # 第五层：符号链接解析与边界检查
        resolved_path = self._resolve_and_check_boundaries(platform_normalized)

        return resolved_path

    # ============ 第一层：长度检查 ============
    def _check_length(self, path: str) -> None:
        """防止过长路径导致的缓冲区溢出或DoS"""
        if len(path) > self.max_path_length:
            raise ValueError(f"路径过长: {len(path)} > {self.max_path_length}")

    # ============ 第二层：URL解码(完全)============
    def _decode_all_encodings(self, path: str) -> str:
        """
        迭代解码直到稳定,防止多重编码攻击

        例： ..%252f -> ..%2f -> ../
        """
        previous = None
        current = path

        # 迭代解码，直到输出不再变化（收敛）。使用迭代上限防止畸形输入。
        # 注意：不能用"% 数量不变"作为提前退出条件，因为 %25xx 解码为 %xx
        # 时 % 数量恰好保持不变，会导致双重编码的路径穿越绕过。
        for _ in range(20):
            if previous == current:
                break
            previous = current
            current = unquote(current)
        else:
            # 20 次仍未收敛，视为攻击输入
            raise ValueError(f"URL 解码未收敛，疑似编码攻击：{path!r}")

        return current

    # ============ 第三层：Unicode规范化 ============
    def _normalize_unicode(self, path: str) -> str:
        """
        Unicode规范化为NFC形式,防止利用不同Unicode形式的绕过

        例： é (U+00E9) 和 e (U+0065) + ◌́ (U+0301) 看起来相同
        都规范化为 NFC: é
        """
        # NFC: 标准分解形式,最常用于文件系统
        return unicodedata.normalize('NFC', path)

    # ============ 第四层：平台特定规范化 ============
    def _normalize_platform(self, path: str) -> str:
        """
        处理平台特定字符
        - Windows: 反斜杠 \ 等同于 /
        - 移除 ./ 当前目录引用
        """
        # 统一使用正斜杠
        path = path.replace('\\', '/')

        # 移除重复的 //
        while '//' in path:
            path = path.replace('//', '/')

        # 使用 os.path.normpath 安全地处理 ./ 和冗余分隔符
        # 注意：不能简单使用 str.replace('./', ''),因为会误伤合法路径
        # 如 "notes./backup" 中的 "./" 并非目录引用
        import posixpath
        path = posixpath.normpath(path)

        return path

    # ============ 第五层：符号链接解析与边界检查 ============
    def _resolve_and_check_boundaries(self, path: str) -> str:
        """
        1. 将相对路径转为绝对路径(相对于base_path)
        2. 解析所有 .. 和 符号链接
        3. 检查最终路径是否仍在base_path内
        """

        # 如果是相对路径,相对于base_path解析
        if not path.startswith('/'):
            full_path = os.path.join(self.base_path, path)
        else:
            full_path = path

        # 使用realpath解析符号链接和..
        # 这是关键步骤：会实际访问文件系统
        try:
            resolved = os.path.realpath(full_path)
        except OSError as e:
            # 如果路径不存在或无法访问,仍然进行规范化
            # 使用abspath代替realpath(不解析符号链接)
            resolved = os.path.abspath(full_path)

        # 规范化base_path
        base_resolved = os.path.realpath(self.base_path)

        # 关键检查：resolved 路径必须在 base_path 内。
        # 使用 commonpath 避免 "/tmp/agent-evil" 或 "..notes" 这类前缀误判。
        if os.path.commonpath([base_resolved, resolved]) != base_resolved:
            raise ValueError(
                f"路径逃逸: {resolved} 不在允许范围 {base_resolved} 内"
            )

        return resolved

    # 辅助方法：白名单检查
    def validate_with_whitelist(self,
                               user_path: str,
                               whitelist: list) -> str:
        """
        可选的白名单检查：仅允许特定目录
        """
        resolved = self.validate(user_path)

        for allowed_dir in whitelist:
            allowed_resolved = os.path.realpath(allowed_dir)
            if resolved.startswith(allowed_resolved + '/') or resolved == allowed_resolved:
                return resolved

        raise ValueError(f"路径 {resolved} 不在白名单内")
```

## 12.4.3 攻击向量验证

让我们用上述PathValidator来验证它对各种攻击的防护：

```python
# 创建测试环境
import tempfile
import os

def test_path_validator():
    """测试路径校验器"""

    # 创建临时目录作为base_path
    with tempfile.TemporaryDirectory() as tmpdir:
        base = os.path.join(tmpdir, "agent")
        os.makedirs(base)

        # 创建一些文件用于测试
        safe_dir = os.path.join(base, "data")
        os.makedirs(safe_dir)
        with open(os.path.join(safe_dir, "safe.txt"), "w") as f:
            f.write("safe content")

        # 在base外创建一个要保护的文件
        protected = os.path.join(tmpdir, "protected.txt")
        with open(protected, "w") as f:
            f.write("protected content")

        # 创建符号链接(Linux)
        try:
            symlink = os.path.join(base, "link_to_protected")
            os.symlink(protected, symlink)
        except:
            symlink = None

        validator = PathValidator(base_path=base)

        # 测试用例
        test_cases = [
            # (输入, 应该成功, 描述)
            ("data/safe.txt", True, "合法的相对路径"),
            ("./data/safe.txt", True, "带./的合法路径"),
            ("data/../data/safe.txt", True, "包含..但仍在范围内"),
            ("../../../etc/passwd", False, "相对路径穿越"),
            ("..%2f..%2fetc%2fpasswd", False, "URL编码穿越"),
            ("..%252f..%252fetc%252fpasswd", False, "双重URL编码穿越"),
            ("data/../../../../../../etc/passwd", False, "多层相对穿越"),
        ]

        for user_path, should_succeed, description in test_cases:
            try:
                result = validator.validate(user_path)
                if should_succeed:
                    print(f"✓ {description}: {user_path} -> {result}")
                else:
                    print(f"✗ {description}: {user_path} 本应被阻止,但成功了")
            except ValueError as e:
                if not should_succeed:
                    print(f"✓ {description}: {user_path} 正确被阻止 ({e})")
                else:
                    print(f"✗ {description}: {user_path} 本应成功,但被拒绝了 ({e})")

# 运行测试
test_path_validator()
```

## 12.4.4 路径校验在工具调用中的应用

实现如下：

```python
from typing import Dict, Any

class ToolCallValidator:
    """工具调用中集成路径校验"""

    def __init__(self, path_validator: PathValidator):
        self.path_validator = path_validator

    def validate_tool_call(self,
                          tool_call: "ToolCall",
                          tool_schema: Dict[str, Any]) -> None:
        """
        在执行工具前,对所有路径类参数进行校验

        假设tool_schema中有"path"字段标记哪些参数是路径
        """

        for param_name, param_value in tool_call.args.items():
            # 检查此参数是否是路径类型
            if param_name in tool_schema.get("path_params", []):
                if isinstance(param_value, str):
                    try:
                        # 校验路径
                        safe_path = self.path_validator.validate(param_value)
                        # 替换为规范化的路径(可选)
                        tool_call.args[param_name] = safe_path
                    except ValueError as e:
                        raise PermissionDenied(f"路径校验失败: {e}")

# 工具schema示例
TOOL_SCHEMAS = {
    "read_file": {
        "description": "读取文件",
        "parameters": {
            "path": {"type": "string", "description": "文件路径"}
        },
        "path_params": ["path"]  # 标记哪些参数是路径
    },
    "list_directory": {
        "description": "列出目录",
        "parameters": {
            "path": {"type": "string", "description": "目录路径"}
        },
        "path_params": ["path"]
    },
    "bash": {
        "description": "执行bash命令",
        "parameters": {
            "command": {"type": "string", "description": "bash命令"}
        },
        "path_params": []  # bash命令中的路径需单独检查
    }
}

# 使用
validator = ToolCallValidator(PathValidator(base_path="/tmp/agent"))

tool_call = ToolCall(
    tool_name="read_file",
    args={"path": "../../../etc/passwd"}
)

try:
    validator.validate_tool_call(tool_call, TOOL_SCHEMAS["read_file"])
except PermissionDenied as e:
    print(f"工具调用被拒绝: {e}")
```

## 12.4.5 路径校验的性能考虑

路径校验涉及多次字符串操作和可能的文件系统调用(realpath)，需要考虑性能。然而，**LRU缓存带来的TOCTOU(Time-of-Check-Time-of-Use)窗口不可忽视**：缓存在validate()时通过，但从缓存命中到实际访问文件之间，文件系统状态可能发生变化（如符号链接被修改、权限改变、文件被删除等）。这个时间窗口虽然通常很小(毫秒级)，但在高并发或对手主动利用的场景中可能被exploited。

建议：

* 缓存TTL应设置较短(≤1秒)，权衡性能与风险
* 关键安全操作（如删除、修改权限）应禁用缓存，每次都调用realpath
* 定期重新验证长生命周期的路径

```python
import time
from functools import lru_cache

class CachedPathValidator(PathValidator):
    """带缓存的路径校验器

    警告：缓存可能导致TOCTOU(检查-使用时间差)风险。
    缓解建议：(1)缓存TTL <= 1秒; (2)关键操作禁用缓存; (3)定期验证。
    """

    def __init__(self, base_path: str = "/tmp/agent", cache_size: int = 1000, cache_ttl: int = 1):
        super().__init__(base_path)
        self.cache_size = cache_size
        self.cache_ttl = cache_ttl  # TTL in seconds
        # 使用LRU缓存,注意必须包装方法以避免无限递归;
        # lru_cache 本身没有 TTL,这里把“时间桶”编入缓存键:
        # 桶号每 cache_ttl 秒变化一次,旧条目自然失效
        self._validate_cached = lru_cache(maxsize=cache_size)(self._validate_bucketed)

    def _validate_bucketed(self, user_path: str, time_bucket: int) -> str:
        del time_bucket  # 仅参与缓存键,用于实现 TTL 过期
        return super().validate(user_path)

    def validate(self, user_path: str) -> str:
        """使用缓存的validate(条目超过 cache_ttl 秒后自动失效)"""
        bucket = int(time.time() / self.cache_ttl)
        return self._validate_cached(user_path, bucket)

    def clear_cache(self):
        """清空缓存"""
        self._validate_cached.cache_clear()

# 性能测试
validator = CachedPathValidator()

paths = ["data/file1.txt", "data/file2.txt"] * 500  # 重复路径

start = time.time()
for path in paths:
    try:
        validator.validate(path)
    except Exception:
        pass
elapsed = time.time() - start

print(f"校验{len(paths)}个路径耗时: {elapsed:.3f}秒")
print(f"缓存命中率: {validator._validate_cached.cache_info()}")
```

## 12.4.6 路径校验最佳实践

1. **始终使用realpath**: 解析符号链接，防止链接逃逸
2. **规范化顺序**: URL解码 → Unicode规范化 → 平台规范化 → realpath
3. **白名单优先**: 相比黑名单，白名单更安全
4. **日志和审计**: 记录所有被拒绝的路径尝试
5. **缓存路径校验**: realpath调用昂贵，使用LRU缓存
6. **定期审查**: 新的编码方式不断出现，定期更新防护

```yaml
## 路径校验配置示例
path_validation:
  enabled: true
  base_path: "/tmp/agent"
  max_path_length: 4096

  # 白名单目录
  whitelist_dirs:
    - "/tmp/agent/data"
    - "/tmp/agent/output"
    - "/tmp/agent/cache"

  # 黑名单目录(绝对黑名单)
  blacklist_dirs:
    - "/etc"
    - "/root"
    - "/var/log"
    - "/.ssh"

  # 缓存设置(TTL 须遵循 12.4.5 的 TOCTOU 建议:≤1 秒)
  cache:
    enabled: true
    max_size: 10000
    ttl_seconds: 1

  # 日志
  audit_log: true
```

***

**本节总结**：路径校验是防护路径穿越的关键。通过 5 层递进式防护（长度、解码、Unicode、平台、realpath），可以有效应对多种已知和未知的编码攻击。关键是：**规范化后必须使用 realpath，并检查最终路径是否在边界内**。
