Code Interpreter临时文件为什么泄露到其他任务?工作目录、对象存储与沙箱生命周期完整排查
文章摘要
代码执行型Agent通常会创建临时脚本、上传附件、解压压缩包、生成图表和导出结果。很多系统虽然为每个请求创建了独立任务ID,却让所有任务共享同一个宿主机目录、容器卷、Python进程工作目录或对象存储前缀。结果可能出现:任务B枚举到任务A上传的合同;缓存复用了其他租户生成的数据文件;容器销毁后宿主机卷仍保留Secrets;下载接口只校验文件名,没有校验文件归属;清理任务失败后敏感Artifact长期滞留。
文件泄露往往不是一个单点漏洞,而是“身份、路径、挂载、对象Key、权限、生命周期、下载令牌和清理审计”多层边界同时缺失。仅使用tempfile、随机UUID或Docker容器并不能自动建立租户隔离。
本文从任务工作区、路径穿越、符号链接、共享卷、容器复用、对象存储前缀、Artifact Registry、下载令牌、文件类型、压缩炸弹、恶意输出、清理状态机和故障恢复等角度给出完整排查方案。
一、一个典型泄露事故
平台为每个任务生成目录:
/workspace/{taskId}代码:
workspace=Path("/workspace")/task_id workspace.mkdir(exist_ok=True)但容器启动时挂载:
-v/data/agent-workspace:/workspace所有租户和任务共享同一个宿主机根目录。如果执行代码能够访问:
Path("/workspace").glob("**/*")就可能看到其他任务。
二、随机目录不是安全边界
UUID只降低猜测难度,不能替代权限。攻击路径包括:
- 枚举父目录;
../路径穿越;- 符号链接;
/proc读取;- 已知文件名;
- 共享缓存;
- 错误下载接口;
- 日志暴露绝对路径;
- 宿主机挂载泄露。
真正的隔离必须由运行时挂载、UID、Namespace、对象权限和应用授权共同完成。
三、文件系统信任边界
至少区分:
用户上传区 沙箱只读输入区 沙箱可写工作区 沙箱临时区 结果待审核区 发布下载区 隔离区 审计元数据区不要使用一个目录承担全部职责。
四、Workspace模型
publicrecordSandboxWorkspace(StringworkspaceId,StringtenantId,StringsubjectId,StringrunId,StringtaskId,WorkspaceStatusstatus,StringruntimeId,InstantcreatedAt,InstantexpiresAt,longquotaBytes,intmaximumFiles){}publicenumWorkspaceStatus{ALLOCATING,READY,EXECUTING,COLLECTING_OUTPUT,QUARANTINED,CLEANING,DELETED,CLEANUP_FAILED}清理不是一个finally中的最佳努力,而是可重试的持久状态。
五、每任务独立运行时
安全默认值:
一个Task →一个短生命周期沙箱 →一个独立可写文件系统 →完成后销毁不要让不可信代码在同一个长期Python进程中连续运行。长期进程会残留:
- 全局变量;
- 导入模块状态;
- 环境变量;
- 当前目录;
- 打开的文件;
- 子进程;
- 网络连接;
- 临时文件;
- 库缓存。
六、容器复用的隐患
如果复用容器:
Task A完成 →清理目录 →Task B进入清理遗漏任何文件、进程或挂载都可能串任务。
建议:
可信同租户低风险任务 可考虑受控复用 不可信代码与跨租户任务 默认不复用七、工作目录不是隔离
设置:
os.chdir(task_workspace)只能改变相对路径解析。代码仍可能访问:
/ ../ /proc /etc 挂载卷 网络文件系统真正隔离依赖运行时权限和挂载命名空间。
八、只挂载任务所需内容
推荐:
/input 只读 /work 可写 /output 可写但由收集器读取 /tmp 每任务独立避免挂载:
- Docker Socket;
- 宿主机根目录;
/var/run;- 云凭证目录;
- 源代码仓库;
- 共享Secret目录;
- 其他任务根目录。
九、只读根文件系统与非Root
容器应配置:
readOnlyRootFilesystem=true runAsNonRoot=true allowPrivilegeEscalation=false只对/work、/tmp和/output提供受控可写挂载。
securityContext:runAsNonRoot:truerunAsUser:10001runAsGroup:10001allowPrivilegeEscalation:falsereadOnlyRootFilesystem:truecapabilities:drop:-ALL不要依赖容器内Root“反正不是真Root”的直觉。
十、Seccomp与Capability
Seccomp限制进程可调用的内核接口。重点审查:
mount;ptrace;- 特权命名空间操作;
keyctl;- 不必要的内核攻击面。
不要将:
seccomp=unconfined作为永久配置。
绝大多数代码解释器不需要:
SYS_ADMIN;NET_ADMIN;SYS_PTRACE;DAC_OVERRIDE。
十一、路径规范化
用户提交输出名:
../../tenant-b/report.csv服务器端必须规范化:
publicPathresolveSafe(PathworkspaceRoot,StringuserName){Pathcandidate=workspaceRoot.resolve(userName).normalize();if(!candidate.startsWith(workspaceRoot)){thrownewPathTraversalException();}returncandidate;}仅检查字符串是否包含..不够,还要处理绝对路径、URL编码、Windows盘符、Unicode分隔符和符号链接。
十二、符号链接攻击
不可信代码创建:
ln-s/input/secret.pdf /output/result.pdf收集器读取/output/result.pdf时,实际读取输入Secret。
输出收集必须:
- 拒绝符号链接;
- 使用
NOFOLLOW; - 检查真实路径;
- 检查文件类型;
- 检查设备号;
- 检查是否位于允许挂载。
十三、输出收集器
publicrecordCollectedArtifact(StringartifactId,StringworkspaceId,StringrelativePath,StringmediaType,longsizeBytes,Stringsha256,ArtifactSecurityStatussecurityStatus){}收集流程:
列举允许目录 →拒绝链接和特殊文件 →大小限制 →类型识别 →恶意内容扫描 →Hash →上传隔离对象存储 →审核 →发布拒绝:
- Socket;
- FIFO;
- Block Device;
- Character Device;
- 稀疏超大文件;
- 未知文件系统对象。
只收集普通文件。
十四、文件数量与大小配额
攻击代码可能生成100万个小文件,或创建一个超大稀疏文件。
限制:
- 最大文件数;
- 单文件大小;
- Workspace总大小;
- Inode;
- 输出数量;
- 压缩后和解压后大小;
- 运行时间。
十五、压缩炸弹
上传ZIP看起来10MB,解压后可能数百GB。
验证:
总展开大小 文件数量 递归层级 压缩比 单文件大小 路径解压必须在隔离环境中进行。
十六、对象存储Key与权限
错误:
bucket/{filename}正确:
tenant/{tenantId}/run/{runId}/ workspace/{workspaceId}/artifact/{artifactId}但前缀仍不是权限本身。访问必须由IAM和应用授权控制。
建议:
- 沙箱不能直接访问整个Bucket;
- 输入使用短期只读预签名URL或代理流;
- 输出只能写指定Key;
- Listing默认禁止;
- 跨租户Key不可访问;
- KMS按租户或敏感级别加密;
- 所有访问审计。
十七、不要注入长期云凭证
危险:
AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY不可信代码可以读取环境变量并外传。
应使用:
- 短期令牌;
- 单一对象权限;
- 最小有效期;
- 网络限制;
- 代理服务;
- Workload Identity并进一步缩权。
十八、Artifact Registry
publicrecordArtifactRecord(StringartifactId,StringtenantId,StringownerSubjectId,StringrunId,StringtaskId,StringworkspaceId,StringstorageKey,StringcontentHash,StringmediaType,longsizeBytes,ArtifactStatusstatus,InstantexpiresAt){}下载不使用用户提供路径,而使用Artifact ID查询归属。
十九、下载接口
GET /api/v1/artifacts/{artifactId}/download服务端检查:
- 当前用户;
- 租户;
- Run权限;
- Artifact状态;
- 保留期;
- 风险;
- 下载次数或用途;
- 审计。
不要提供:
GET /download?path=/data/...二十、下载令牌
publicrecordArtifactDownloadToken(StringtokenId,StringartifactId,StringtenantId,StringsubjectId,Stringpurpose,InstantexpiresAt,intmaximumDownloads){}令牌应短期、一次性或限次。
二十一、文件名仅用于展示
存储Key使用内部ID。
用户原文件名需要:
- 清理控制字符;
- 限制长度;
- 仅作为Content-Disposition;
- 防止Header注入;
- 不作为路径。
二十二、MIME不能只信扩展名
report.pdf可能实际是脚本或可执行文件。
同时检查:
- Magic Bytes;
- MIME;
- 扩展名;
- 内容策略;
- 扫描结果。
二十三、生成HTML与SVG的风险
Agent输出HTML可能包含:
- Script;
- 外链资源;
- 表单;
- 自动跳转;
- 恶意SVG;
- 浏览器漏洞利用。
展示前:
- 下载而非内联;
Content-Disposition: attachment;- 使用独立域名;
- 配置CSP;
- HTML净化;
- 禁止Cookie;
- 禁止与主站同源。
二十四、高风险文件格式
高风险格式包括:
- Pickle;
- Joblib;
- 不可信Notebook;
- Office宏;
- 可执行脚本;
- 带外部引用文件。
不要在主服务中反序列化不可信对象。
二十五、日志和错误页泄露
日志可能暴露:
workspace=/data/tenant-a/contract-123或记录完整命令和文件内容。
日志应使用:
workspace_id artifact_id relative_path_hash避免绝对宿主机路径和敏感文件名。
用户错误页也不应暴露:
- 容器ID;
- Bucket;
- Secret路径;
- 内核信息;
- 内部服务地址。
详细信息只进入受控Trace。
二十六、网络外传
即使文件系统隔离,不可信代码仍可通过网络上传文件。
默认策略:
无网络需要网络时:
- 域名Allowlist;
- DNS代理;
- 禁止私网地址;
- 禁止云Metadata;
- 限制端口;
- 限制流量;
- TLS策略;
- 请求审计。
二十七、SSRF保护
禁止访问:
127.0.0.0/8;- RFC1918私网;
- Link-local;
- 云Metadata;
- Kubernetes Service网络;
- 管理平面;
- 数据库。
同时防止DNS Rebinding、IPv6绕过、重定向到私网和混淆IP表示。
二十八、进程与资源限制
设置:
- PID上限;
- CPU;
- 内存;
- Wall Time;
- 文件描述符;
- 子进程数;
- 线程;
- 磁盘IO;
- 网络带宽。
防止Fork Bomb、资源耗尽和无限输出。
二十九、清理不是删除目录这么简单
清理对象包括:
- 容器或MicroVM;
- Workspace;
- 临时卷;
- 对象存储;
- Token;
- 网络策略;
- 租约;
- 进程;
- 下载令牌;
- 缓存。
三十、清理状态机
publicrecordCleanupJob(StringcleanupId,StringworkspaceId,CleanupStatusstatus,intattempt,List<String>remainingResources,InstantnextAttemptAt,StringlastError){}清理失败不能把Workspace直接标记删除:
CLEANUP_FAILED →隔离 →告警 →重试 →人工处置在完成清理前,不得复用运行时。
三十一、TTL与业务保留
不同数据需要不同策略:
临时脚本:任务后立即删除 原始上传:按用户政策 输出Artifact:按产品保留 安全隔离样本:按事故政策 审计Hash:长期不要统一一个TTL。
三十二、用户删除请求
必须传播到:
- Artifact Registry;
- 对象存储;
- Checkpoint引用;
- Replay包;
- 缓存;
- 搜索索引;
- 备份策略。
业务审计可以保留最小Hash和删除事件。
三十三、沙箱镜像
使用:
- 最小基础镜像;
- 固定Digest;
- SBOM;
- 漏洞扫描;
- 只安装必要包;
- 定期更新;
- 禁止运行时任意安装系统包。
动态Python包安装应经过内部代理、Allowlist和缓存仓库。
三十四、依赖供应链
用户代码执行:
pipinstallsome-package可能拉取恶意包。
策略:
- 默认禁止联网安装;
- 内部镜像仓库;
- 包Allowlist;
- Hash固定;
- 版本锁定;
- 安装发生在隔离构建层;
- 结果不复用到其他租户。
三十五、租户隔离等级
低风险可信内部代码
容器+独立Workspace中风险用户脚本
加固容器+严格网络+独立节点池高风险公开代码
MicroVM或专用远程执行池极高风险
不提供执行 或人工离线环境三十六、运行Manifest
publicrecordSandboxExecutionManifest(StringexecutionId,StringtenantId,StringworkspaceId,StringimageDigest,StringisolationProfile,StringnetworkPolicyVersion,StringfilesystemPolicyVersion,ResourceLimitslimits,List<ArtifactRef>inputs,InstantstartedAt){}三十七、Trace
sandbox.allocate sandbox.stage_input sandbox.execute sandbox.collect_output sandbox.scan_output sandbox.publish_artifact sandbox.cleanup三十八、指标
sandbox_workspace_total{ status } sandbox_cross_workspace_access_denied_total sandbox_path_traversal_denied_total sandbox_symlink_output_denied_total sandbox_quota_exceeded_total{ type } sandbox_output_quarantined_total{ reason } sandbox_cleanup_total{ result } sandbox_cleanup_age_seconds sandbox_artifact_download_denied_total{ reason } sandbox_network_denied_total{ destination_class }三十九、自动化测试
至少覆盖:
Task B枚举Workspace根目录 路径包含../../ 输出符号链接到输入Secret 创建特殊文件 压缩炸弹 跨租户Artifact下载 访问云Metadata 清理中服务重启 容器复用残留 对象存储Grant过期四十、最终排查清单
□ 每个Task拥有独立Workspace ID □ 不可信代码运行时不跨租户复用 □ 输入只读、工作区和输出区分离 □ Root Filesystem只读 □ 非Root、无提权、Capability全删 □ Seccomp和网络策略启用 □ 不挂载Docker Socket和宿主机敏感目录 □ 路径解析使用规范化与根目录校验 □ 输出收集拒绝符号链接和特殊文件 □ 文件数、大小、Inode和展开大小有限额 □ 对象存储Key包含租户、Run与Artifact □ 沙箱不能List整个Bucket □ 不注入长期云凭证 □ 下载以Artifact ID授权而非文件路径 □ HTML和SVG在隔离域展示 □ 默认无网络,开放时使用Allowlist □ 清理有持久状态和重试 □ 清理失败的运行时不会复用 □ Trace不记录敏感绝对路径 □ 测试覆盖路径、链接、下载和清理故障总结
Code Interpreter文件泄露并不是“临时目录没有随机化”这么简单,而是系统没有把文件当成跨越沙箱、对象存储、下载接口和生命周期的受控Artifact。
可靠文件隔离需要:
每任务独立运行时 +最小挂载 +非Root与系统调用限制 +版本化Artifact Registry +服务端下载授权 +网络外传防护 +可恢复清理状态机真正的安全目标不是“任务结束后大概率删掉文件”,而是确保任何任务在执行、恢复、下载和清理的每个阶段,都无法读取、覆盖或发布不属于自己的数据。
