先把工作区当作能力边界
文件工具的安全起点不是“提示模型不要访问外部目录”,而是为每个任务分配一个操作系统真正可以强制的根。工作进程应使用专用的低权限身份,只对该任务目录拥有必要读写权,不继承宿主用户的云凭据、开发密钥和配置目录访问权。即使工具参数校验出现缺陷,内核权限也应把影响限制在任务边界内。
工作区的生命周期要与任务标识绑定:使用不可预测的独立目录创建,写入一份允许的输入清单,执行中记录文件数量与字节配额,结束后只回收该标识对应的精确路径。不要让代理自己选择或声明根目录,也不要将一个共享临时目录长期复用给不同租户。随机名称只是防猜测的辅助,真正的隔离仍来自身份、挂载和访问控制。
路径规范化不能使用字符串前缀
用“解析后路径是否以工作区字符串开头”判断边界会忽略组件分隔、大小写规则和平台特性。例如根为某个目录时,同名前缀的兄弟目录不应被接受。服务端应先拒绝绝对路径、空字节和不支持的编码,再按目标平台规则分解相对组件,消解点与上级组件,最后用路径组件关系而非文本相似度确认它仍在根内。
跨平台服务还应把盘符、网络共享路径、反斜杠和保留设备名称纳入测试,而不是拿在一个系统上的解析结果推断另一个系统。上传文件的原始名称只能作为展示元数据,真实落盘名应由服务端产生。如果业务需要保留目录结构,就逐层创建并校验每个组件,不把用户提供的整段路径直接交给系统调用。
符号链接使检查后的路径仍然可变
即使规范化后的文本位于根目录,路径中任意一层都可能是指向外部的符号链接。更难处理的是检查与打开之间的竞态:攻击者在服务完成真实路径检查后替换某个目录为链接,随后的写入就落到工作区之外。因此“先查再用”不是原子安全性,检查成功也不能被当成后续所有路径操作的通行证。
更稳妥的做法是从已信任的工作区目录句柄出发,逐层以相对名称打开子项,在系统提供能力时禁止跟随链接,并将后续读写绑定到已打开句柄而非再次解析名称。对必须允许链接的业务,则要明确链接可指向的资源集合,查验最终对象类型和设备边界,且不允许一般模型输出决定这个例外。
解压缩包需要另一套边界
压缩包条目中的文件名同样是不可信输入。条目可以携带上级组件、绝对路径、链接或特殊文件类型,如果库直接按名称释放,普通的导入任务就会变成越界写入。解压器应在写入前逐项规范化目标,只允许常规文件和必要目录,禁止设备、管道和链接,并在打开目标时延续工作区句柄的边界。
仅限制压缩包上传字节还不够,因为少量压缩数据可以展开为极大内容或极多空文件。在读取目录时就累计条目数、声称展开大小、路径深度和单文件上限,真正解码时再以实际写入字节作第二道计数。超限时终止整个管线并清理本次产物,不把已释放的半成品暴露给后续代理步骤。
子进程隔离要覆盖资源与凭据
文件任务常会启动编译器、转换器或用户项目中的脚本,而这些程序的风险远高于简单读取。子进程环境应从空白允许列表构建,不继承云厂商凭据、代理设置、宿主家目录和开发工具配置。根文件系统只挂载必要的只读运行时与可写工作区,如无业务需求则关闭网络,避免被处理文件将本地内容外传。
资源配额应同时包含处理器时间、实际内存、进程数、打开句柄、写入字节和墙钟超时。只设置一个总超时无法阻止子进程在短时间内吃尽内存,只限制内存又可能被大量小文件和句柄拖垮宿主。超限时要终止整个进程树,等待子级退出后再回收目录,否则清理程序会与尚在写入的后代形成新竞态。
写入提交必须与临时产物分层
代理生成的文件不应一边写一边就被其他服务读到。先在工作区内使用独占方式创建临时文件,完成字节上限、格式、权限和内容安全检查后,再在同一文件系统内以原子替换提交到公开位置。目标已存在时要明确选择拒绝、条件覆盖或创建新版本,绝不能因为工具默认覆盖就接受模型的模糊意图。
临时文件名和权限需要在创建时确定,不要先创建宽权限文件后再尝试收紧。提交时如果需要保留原文件,应通过稳定版本标识及内容哈希确认自检查后没有被其他进程替换。跨文件的修改通常没有单一文件系统交易,因此应保存操作清单和每项提交证据,断电恢复时按真实文件状态继续,而不盲目重放整个计划。
清理逻辑本身也要被沙箱约束
任务失败后的递归删除是最危险的路径之一,因为它通常处于异常分支,测试较少,影响却比普通写入更大。清理前重新解析任务标识对应的服务端记录,将精确绝对路径与预期工作区父目录做组件级校验,拒绝空值、根目录、家目录和未解析变量。不要从模型输出、请求查询或模糊匹配中推导待删除根。
为了阻止清理遇到被替换的链接或挂载点,可先遍历并检查对象类型,但仍需依赖平台提供的不跟随语义或句柄相对操作减少竞态。重要产物优先移入受控回收区并设置保留期,而不是立即永久删除。清理完成后记录删除项数、总字节和未能处理的精确相对路径,日志不复制任务文件内容。
用攻击性文件树做验证矩阵
测试集要主动构造嵌套上级路径、绝对路径、同名前缀目录、指向根外的文件链接、指向根外的目录链接、链接环和过深目录。竞态测试在路径校验与打开之间高频替换目录,断言根外蜂鸣文件的内容、时间和元数据始终不变。压缩测试覆盖条目数爆炸、高压缩比、链接条目和路径编码差异。
资源与恢复矩阵需在文件写入中、原子替换前后、子进程创建后和清理期间强制终止任务。每个时点都要检查没有留存子进程、打开句柄、公开半成品或越界文件。再以并发任务验证处理器、内存、磁盘和文件数配额是按任务隔离的,某个恶意输入只导致它自身受控失败,不能拖垮已在运行的其他工作区。
文件类型与硬链接不能被名称遮蔽
扩展名不能证明文件类型,一个名为图片的路径可能实际是管道、设备、套接字或指向其他对象的链接。工具在打开后从文件句柄获取类型和元数据,只允许当前操作声明的常规文件或目录。对不支持的类型立即关闭句柄并返回脱敏错误,不尝试读取以猜测内容,否则管道可以让任务无限阻塞,设备文件则可能将不预期数据带入代理。
硬链接不像符号链接那样在路径中显眼,两个名称可以指向同一底层对象。若工作区允许从外部导入已存在的硬链接,代理修改它可能同时改变另一个信任位置的文件。创建工作区时应使用独立文件系统或复制受信任输入,对关键写入检查链接计数与设备标识。读取安全和写入安全要分开评估:一个可读的共享输入并不自然可以原地修改。
读取、写入和执行应使用独立能力
一个能读取项目文件的代理不应因此获得原地覆盖与执行权。将工具契约拆成只读打开、在临时区创建、经验证后提交和受控执行四种能力,服务端根据任务类型分别授权。例如文档总结任务只需只读挂载和一个独立输出目录,不应获得项目源码写入权。能力标识绑定任务、根句柄、方法和有效期,不使用一份可跨工作区复用的通用令牌。
执行脚本时将文件可读与进程可执行分开,工作区中用户上传和代理生成的文件默认不具有执行权。如果业务真正需要运行项目命令,使用明确的可执行允许列表、固定工作目录和参数数组,不将模型产生的整段命令交给命令解释器。执行环境挂载输入为只读,产物写入另一目录,可以避免构建脚本悄然改写它正在分析的源文件。
沙箱逃逸之外还要测试宿主耗尽
一个任务即使始终没有读写根外路径,仍可以通过快速创建文件、日志、子进程或内存映射耗尽共享宿主。配额要同时在任务层和宿主层生效:任务超限时仅终止它自身,宿主接近保护阈值时拒绝新任务而不随机杀死其他租户进程。文件数与目录项数要单独限制,因为数百万个空文件可以在占用字节很少时拖垮遍历、备份和清理。
验证矩阵使用高压缩比包、海量小文件、无限子进程、慢速输出和不断打开句柄的程序,观察任务被终止后资源是否返还基线。在多租户并发情况下,让一个恶意任务连续命中每项配额,同时检查普通任务的延迟和成功率。还要在清理中断与宿主重启后扫描孤立工作区,根据持久化任务标识安全回收,不依赖已消失的内存计数。
实现片段
const target = await workspace.openRelative(relativePath, flags)
评论 · 0