主题
文件沙箱与安全控制
本文引用的源码文件
本文引用的文件
目录
- 引言
- 项目结构与安全边界
- 核心安全组件
- 架构总览
- 路径白名单与规范化机制
- 目录遍历防护与符号链接检测
- 权限模型与工作目录特殊处理
- 命令分级管控与文件沙箱的边界
- 完整路径安全检查流程
- 依赖关系分析
- 性能与资源限制
- 故障排查指南
- 安全最佳实践
- 结论
引言
本文聚焦 Agent 端的文件沙箱与安全控制,目标是说明:
- 路径输入如何被净化、规范化、硬阻止检查、白名单匹配和符号链接验证。
normalizePathFormat、allowedPaths、workdir、MEMORY_DIR、HARD_BLOCKED_PATHS、isHardBlocked等关键概念如何协作。- 文件沙箱与命令分级管控的关系,以及当前实现的能力边界。
- 扩展端工具定义与 Agent 端执行器之间的职责划分。
- 面向生产的安全建议、审计日志和资源限制要点。
本项目采用双进程架构:Chrome 扩展端负责用户交互、工具调度与网络访问策略;Agent 端(Node.js)负责本地文件系统、命令执行、长期记忆与 RAG。版本以 2.3.1 为准,Agent 源码修改后必须重启进程,新接口才生效。
项目结构与安全边界
从安全视角看,本仓库中与文件沙箱直接相关的代码主要分布在以下位置:
- Agent 端安全核心:
agent/src/security.js - Agent 端配置与默认工作目录:
agent/src/config.js - Agent 端命令执行器:
agent/src/executor.js - 扩展端 Agent 工具定义:
src/background/tools/agent-tools.js - 扩展端工具执行与确认流程:
src/background/tool-executor.js
graph TB Extension["扩展端<br/>tool-executor.js"] --> ToolDef["工具定义<br/>agent-tools.js"] Extension --> AgentClient["本地 Agent 客户端"] AgentClient --> AgentServer["Agent 服务端"] AgentServer --> Security["安全模块<br/>security.js"] AgentServer --> Config["配置模块<br/>config.js"] AgentServer --> Executor["命令执行器<br/>executor.js"] Security --> FS["操作系统文件系统"] Config --> FS Executor --> FS
图表来源
- src/background/tool-executor.js#L1-L23
- src/background/tools/agent-tools.js#L1-L25
- agent/src/security.js#L1-L10
- agent/src/config.js#L1-L20
- agent/src/executor.js#L1-L10
章节来源
- src/background/tool-executor.js#L1-L23
- src/background/tools/agent-tools.js#L1-L25
- agent/src/security.js#L1-L10
- agent/src/config.js#L1-L20
- agent/src/executor.js#L1-L10
核心安全组件
Agent 端的安全能力集中在 security.js,它同时承担两类职责:
- 文件沙箱:校验文件路径是否属于允许范围,防止越权读取、写入、删除或下载。
- 命令分级管控:对 shell 命令进行黑名单、灰名单与强制放行判断。
此外,config.js 提供默认工作目录、允许路径列表、记忆存储目录等基础配置;executor.js 负责命令执行、超时控制、输出缓冲与进程生命周期管理;agent-tools.js 暴露给扩展端的 Agent 工具定义;tool-executor.js 在扩展端组织工具调用、用户确认与审计记录。
章节来源
- agent/src/security.js#L1-L10
- agent/src/config.js#L15-L39
- agent/src/executor.js#L1-L10
- src/background/tools/agent-tools.js#L3-L25
- src/background/tool-executor.js#L1-L23
架构总览
下图展示一次“Agent 文件操作”的典型调用链:扩展端工具定义 → 扩展端工具执行 → 本地 Agent 客户端 → Agent 服务端 → 安全模块 → 文件系统。
sequenceDiagram participant LLM as "大模型" participant Ext as "扩展端工具执行器" participant Agent as "本地 Agent 客户端" participant Server as "Agent 服务端" participant Sec as "安全模块" participant FS as "文件系统" LLM->>Ext : 调用 agent_file / agent_exec Ext->>Agent : 发起 HTTP/WebSocket 请求 Agent->>Server : 转发到对应 API Server->>Sec : 传入 path/command 做安全检查 Sec-->>Server : 返回 allowed/denied/confirm Server->>FS : 通过后再执行读/写/删/查 FS-->>Server : 返回结果或错误 Server-->>Ext : 返回最终结果 Ext-->>LLM : 格式化后的结果
图表来源
- src/background/tools/agent-tools.js#L3-L25
- src/background/tool-executor.js#L1-L23
- agent/src/security.js#L109-L229
路径白名单与规范化机制
normalizePathFormat:跨平台路径规范化
normalizePathFormat 是路径进入沙箱前的第一道规范化步骤,主要解决 Windows、MSYS/Git Bash 与跨平台分隔符差异:
- 修正异常盘符前导斜杠,例如
/C:/归一为C:/。 - 将 MSYS 风格路径
/d/Users/...转换为 Windows 盘符形式D:/Users/...。 - 统一反斜杠为正斜杠,交给 Node.js
path.resolve/normalize做最终归一。 - 将 Windows 盘符统一大写,避免小写盘符与大写白名单前缀比较失败。
该函数支持单元测试时通过 platformOverride 覆盖平台行为,保证测试可预测。
章节来源
allowedPaths:配置解析与自动追加
allowedPaths 来自 config.js 中的 loadConfig():
- 默认值为
[WORKSPACE_DIR],即.ai-helper-agent/workspace。 - 如果配置文件不存在,会创建默认配置并缓存。
- 加载配置后,会自动把
SKILLS_DIR和WORKSPACE_DIR加入allowedPaths,确保技能目录与工作目录始终可访问。 - 当
allowedPaths为空时,安全模块会回退到使用workdir。
这意味着:
- 用户可以在
config.json中自定义允许路径。 - 但系统不会移除内置的工作目录和技能目录。
- 若用户清空
allowedPaths,实际仍会保留工作目录作为兜底。
章节来源
workdir:工作目录的特殊放行逻辑
workdir 是用户当前选定的操作目录,由 config.workdir 决定,默认等于 WORKSPACE_DIR。安全模块对 workdir 有特殊处理:
- 即使
workdir不在allowedPaths中,只要目标路径落在workdir下,就优先放行。 - 放行前会对
resolved路径调用realpath,检查符号链接是否指向硬阻止目录。 - 如果路径不存在(ENOENT),且父目录可解析,则尝试构造真实路径继续校验;无法解析时拒绝。
- 如果符号链接绕过到硬阻止目录,直接拒绝。
这体现了“用户明确选择的工作目录应始终可用,但不能绕过核心敏感文件保护”的设计。
章节来源
目录遍历防护与符号链接检测
HARD_BLOCKED_PATHS:硬阻止清单
HARD_BLOCKED_PATHS 列出任何情况下都不可访问的核心敏感文件和目录,包括:
- Agent 配置文件
config.json - 配对记录
pairings.json - MCP 服务器配置
mcp_servers.json - 日志目录
logs - 回收站目录
.trash - 内置技能状态
builtin_skills_state.json - 停用技能状态
disabled_skills.json
这些路径不会被白名单绕过,也不会因 workdir 特殊处理而放行。
章节来源
isHardBlocked:大小写不敏感比较
Windows 文件系统不区分大小写,因此 isHardBlocked 在比较时会:
- 根据平台决定是否转为小写。
- 对目录型硬阻止项,检查是否以该目录开头或完全相等。
- 对文件型硬阻止项,要求精确匹配。
这样可防止通过 .AI-HELPER-AGENT/CONFIG.JSON 等大小写变换绕过硬阻止。
章节来源
realpath:符号链接检测与绕过防护
checkPath 在多个阶段使用 realpath:
- 对
workdir内路径先做符号链接检测,再决定是否放行。 - 对普通白名单路径,先做前缀匹配,再解析真实路径。
- 对真实路径再次做硬阻止检查。
- 对真实路径再次做白名单前缀检查,防止符号链接跳转到其他允许目录。
当路径不存在时,实现会向上回溯父目录,尝试解析已存在的父路径,再拼接剩余部分;如果所有父目录都无法解析,则拒绝。
flowchart TD Start(["开始检查路径"]) --> Normalize["规范化路径<br/>normalizePathFormat + resolve + normalize"] Normalize --> HardCheck["硬阻止检查<br/>isHardBlocked(normalized)"] HardCheck --> |命中| DenyHard["拒绝:命中核心敏感文件"] HardCheck --> |未命中| MemoryCheck["检查 MEMORY_DIR"] MemoryCheck --> |命中| AllowMemory["放行:记忆存储目录"] MemoryCheck --> |未命中| WorkdirCheck["检查工作目录"] WorkdirCheck --> |命中| RealpathWorkdir["realpath 符号链接检测"] RealpathWorkdir --> |绕过硬阻止| DenyHard RealpathWorkdir --> |未绕过| AllowWorkdir["放行:工作目录"] WorkdirCheck --> |未命中| AllowedPathsCheck["allowedPaths 前缀匹配"] AllowedPathsCheck --> |未命中| DenyNotInAllowed["拒绝:不在允许目录"] AllowedPathsCheck --> |命中| RealpathAllowed["realpath 解析真实路径"] RealpathAllowed --> RealpathHard["对 realPath 做硬阻止检查"] RealpathHard --> |命中| DenyHard RealpathHard --> |未命中| RealpathAllowedPrefix["对 realPath 再做白名单前缀检查"] RealpathAllowedPrefix --> |命中| AllowFinal["放行:最终授权"] RealpathAllowedPrefix --> |未命中| DenyOutOfRange["拒绝:真实路径超出范围"]
图表来源
章节来源
权限模型与工作目录特殊处理
MEMORY_DIR:记忆存储目录自动放行
MEMORY_DIR 是 Agent 内部管理的长期记忆存储目录,位于 .ai-helper-agent/memory。安全模块将其视为系统级受信任目录:
- 一旦路径命中
MEMORY_DIR,直接放行。 - 不需要经过
allowedPaths配置。 - 这是合理的,因为记忆数据由 Agent 自身管理,不应被外部白名单策略阻断。
注意:MEMORY_DIR 的放行发生在硬阻止检查之后,因此仍可防止直接访问硬阻止文件。
章节来源
系统目录保护
系统目录保护主要通过两种方式实现:
- 硬阻止清单:禁止访问 Agent 配置、配对、日志、回收站、技能状态等核心文件。
- 命令层拦截:命令分级管控会阻止破坏性系统命令,如递归删除系统目录、格式化磁盘、覆盖
/etc/passwd、fork bomb 等。
需要明确的是:文件沙箱的 allowedPaths 只约束文件类工具;命令通道本身不受 allowedPaths 约束,因此像 cat /etc/passwd 这类“读取即外泄”的动作,应由更上层的网络/命令策略和用户确认机制控制。
章节来源
工作目录权限模型
工作目录权限模型可以概括为:
workdir是用户当前上下文的操作根目录。- 所有相对路径都会基于
workdir解析。 workdir下的路径优先放行,但仍需通过符号链接检测。- 非
workdir的路径必须命中allowedPaths。 - 所有路径最终都要通过
realpath的真实路径校验。
flowchart TD
Input["输入路径"] --> Relative{"是否相对路径"}
Relative --> |是| ResolveWorkdir["基于 workdir 解析绝对路径"]
Relative --> |否| UseInput["直接使用输入路径"]
ResolveWorkdir --> Normalize["规范化"]
UseInput --> Normalize
Normalize --> Decision["授权决策"]
Decision --> |workdir 内| FastAllow["快速放行 + 符号链接校验"]
Decision --> |allowedPaths 内| PrefixCheck["白名单前缀匹配"]
PrefixCheck --> RealpathVerify["realpath 真实路径验证"]
RealpathVerify --> FinalDecision["最终授权"]
图表来源
章节来源
命令分级管控与文件沙箱的边界
黑名单与灰名单
命令分级管控分为三类:
- 黑名单:绝对禁止,不可绕过。例如递归删除系统目录、格式化磁盘、覆盖系统关键文件、fork bomb、shebang 直接执行、恶意管道执行等。
- 灰名单:需要用户确认。例如
sudo、rm -rf、chmod 777、网络外联、脚本解释器执行外部文件等。 - 强制放行:用户确认后跳过灰名单,但黑名单仍然生效。
子命令拆分与包装器剥壳
命令不是只看整串开头,而是:
- 按 shell 控制符拆分为子命令段。
- 剥离
nohup、sudo、env、timeout等包装器。 - 提取
-c/-e、/c /k、PowerShell-Command/-EncodedCommand的内联载荷。 - 提取
$( … )和反引号内的子命令。 - 对路径参数做归一化,防止
//usr、/.//usr等写法绕过。
文件沙箱与命令通道的边界
文档明确指出:命令通道不受 allowedPaths 约束。也就是说,文件沙箱不能替代命令安全网关。对于高危命令,应依赖黑名单、灰名单和用户确认流程。
章节来源
- agent/src/security.js#L231-L244
- agent/src/security.js#L285-L384
- agent/src/security.js#L386-L447
- agent/src/security.js#L644-L758
完整路径安全检查流程
综合前面的分析,路径安全检查的完整流程如下:
- 输入验证:检查是否为字符串,空值直接拒绝。
- 展开用户主目录:支持
~和~/path。 - 路径规范化:调用
normalizePathFormat,然后resolve+normalize。 - 硬阻止检查:优先检查是否命中
HARD_BLOCKED_PATHS。 - 记忆目录放行:命中
MEMORY_DIR直接放行。 - 工作目录放行:命中
workdir放行,但需realpath检测符号链接。 - 白名单匹配:检查是否在
allowedPaths前缀范围内。 - 符号链接验证:解析真实路径,再次检查硬阻止和白名单。
- 最终授权决策:返回
allowed、resolved或拒绝原因。
flowchart TD
A["输入 pathStr"] --> B["类型与空值校验"]
B --> C["展开 ~ 到 home 目录"]
C --> D["normalizePathFormat 规范化"]
D --> E["resolve + normalize 得到 normalized"]
E --> F{"是否命中 HARD_BLOCKED_PATHS"}
F --> |是| G["拒绝:核心敏感文件"]
F --> |否| H{"是否命中 MEMORY_DIR"}
H --> |是| I["放行:记忆存储目录"]
H --> |否| J{"是否命中 workdir"}
J --> |是| K["realpath 符号链接检测"]
K --> |绕过硬阻止| G
K --> |未绕过| L["放行:工作目录"]
J --> |否| M["allowedPaths 前缀匹配"]
M --> |未命中| N["拒绝:不在允许目录"]
M --> |命中| O["realpath 解析真实路径"]
O --> P{"realPath 是否命中 HARD_BLOCKED_PATHS"}
P --> |是| G
P --> |否| Q{"realPath 是否命中 allowedPaths"}
Q --> |是| R["放行:最终授权"]
Q --> |否| S["拒绝:真实路径超出范围"]
图表来源
章节来源
依赖关系分析
graph LR Security["security.js"] --> Path["Node path 模块"] Security --> FsPromises["fs/promises.realpath"] Security --> Os["os.homedir / os.platform"] Security --> Config["config.loadConfig"] Security --> I18n["i18n.translate"] Config --> FsSync["fs.readFileSync/writeFileSync/mkdirSync"] Config --> Crypto["crypto-lite.encodeSecret/decodeSecret"] Executor["executor.js"] --> ChildProcess["child_process.spawn"] Executor --> Config Executor --> I18n Executor --> Os
图表来源
章节来源
性能与资源限制
Agent 端在执行命令时实现了多项资源限制:
- 输出缓冲区上限:stdout/stderr 最多缓存 5MB,超过后截断并广播截断提示。
- 命令超时策略:支持基础超时、延长次数上限和总预算封顶。
- 静默进程终止:如果在预算期内无输出,视为挂起或静默失败,到期即杀。
- 环境变量白名单:子进程只继承必要环境变量,避免泄露宿主敏感信息。
- 进程树清理:Windows 使用
taskkill /T /PID,Unix 使用负 PID 终止进程组。
这些机制与文件沙箱共同构成 Agent 端的安全基线:文件访问受路径白名单约束,命令执行受分级管控和资源限制约束。
章节来源
- agent/src/executor.js#L96-L114
- agent/src/executor.js#L118-L156
- agent/src/executor.js#L165-L186
- agent/src/executor.js#L495-L549
故障排查指南
路径被拒绝但确实在工作目录下
可能原因:
- 路径包含符号链接,且符号链接指向硬阻止目录。
- 路径尚未存在,父目录也无法解析。
- 路径大小写或分隔符导致规范化后不匹配。
排查建议:
- 检查路径是否经过
realpath解析。 - 检查是否存在符号链接。
- 检查
workdir是否与预期一致。
章节来源
白名单配置未生效
可能原因:
- 配置文件损坏或解析失败,回退到默认配置。
allowedPaths被自动追加了SKILLS_DIR和WORKSPACE_DIR。- 路径未正确规范化。
排查建议:
- 检查
config.json是否可解析。 - 检查
loadConfig是否返回默认配置。 - 检查路径是否经过
normalizePathFormat。
章节来源
命令被拒绝但认为风险不高
可能原因:
- 命中黑名单,黑名单不可绕过。
- 命中灰名单,但未走用户确认流程。
- 命令包含内联载荷或子命令替换。
排查建议:
- 检查命令是否命中
BLACKLIST_PATTERNS。 - 检查是否命中
GRAYLIST_PATTERNS。 - 检查命令是否包含
$( … )、反引号、-c/-e、-Command等特征。
章节来源
安全最佳实践
输入净化
- 始终对路径进行类型校验,拒绝非字符串输入。
- 对用户输入的
~展开为主目录,避免误判。 - 使用
normalizePathFormat统一跨平台路径格式。 - 使用
resolve+normalize消除..、.、重复分隔符。
输出验证
- 对文件读取结果进行长度和内容校验。
- 对命令输出设置缓冲区上限,防止内存耗尽。
- 对截断输出广播提示,避免上层误以为完整成功。
资源限制
- 为命令执行设置合理超时。
- 对长时间无输出的命令及时终止。
- 限制子进程环境变量,避免敏感信息泄露。
- 使用进程组终止,防止僵尸进程残留。
审计日志记录
扩展端工具执行会记录审计日志,包括工具名称、参数、结果、错误信息等。建议在 Agent 端也记录:
- 每次路径检查的结果。
- 被拒绝的路径和原因。
- 被拒绝的命令和分类。
- 用户确认后的强制放行记录。
章节来源
结论
Agent 端的文件沙箱与安全控制围绕“路径白名单 + 硬阻止清单 + 符号链接检测 + 工作目录特殊处理”构建,形成多层防御:
normalizePathFormat解决跨平台路径差异。allowedPaths提供可配置的访问范围。workdir保证用户当前工作目录始终可用。MEMORY_DIR作为系统级受信任目录自动放行。HARD_BLOCKED_PATHS与isHardBlocked防止核心敏感文件被绕过。realpath防止符号链接逃逸。- 命令分级管控补充文件沙箱无法覆盖的命令安全风险。
在生产环境中,应结合输入净化、输出验证、资源限制和审计日志,形成完整的可信执行边界。同时需注意:Agent 源码修改后必须重启进程,新接口才生效;命令通道不受 allowedPaths 约束,不能仅依赖文件沙箱保护系统安全。