Code Interpreter临时文件为什么泄露到其他任务?工作目录、对象存储与沙箱生命周期完整排查
文章摘要代码执行型Agent通常会创建临时脚本、上传附件、解压压缩包、生成图表和导出结果。很多系统虽然为每个请求创建了独立任务ID却让所有任务共享同一个宿主机目录、容器卷、Python进程工作目录或对象存储前缀。结果可能出现任务B枚举到任务A上传的合同缓存复用了其他租户生成的数据文件容器销毁后宿主机卷仍保留Secrets下载接口只校验文件名没有校验文件归属清理任务失败后敏感Artifact长期滞留。文件泄露往往不是一个单点漏洞而是“身份、路径、挂载、对象Key、权限、生命周期、下载令牌和清理审计”多层边界同时缺失。仅使用tempfile、随机UUID或Docker容器并不能自动建立租户隔离。本文从任务工作区、路径穿越、符号链接、共享卷、容器复用、对象存储前缀、Artifact Registry、下载令牌、文件类型、压缩炸弹、恶意输出、清理状态机和故障恢复等角度给出完整排查方案。一、一个典型泄露事故平台为每个任务生成目录/workspace/{taskId}代码workspacePath(/workspace)/task_id workspace.mkdir(exist_okTrue)但容器启动时挂载-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容器应配置readOnlyRootFilesystemtrue runAsNonRoottrue allowPrivilegeEscalationfalse只对/work、/tmp和/output提供受控可写挂载。securityContext:runAsNonRoot:truerunAsUser:10001runAsGroup:10001allowPrivilegeEscalation:falsereadOnlyRootFilesystem:truecapabilities:drop:-ALL不要依赖容器内Root“反正不是真Root”的直觉。十、Seccomp与CapabilitySeccomp限制进程可调用的内核接口。重点审查mountptrace特权命名空间操作keyctl不必要的内核攻击面。不要将seccompunconfined作为永久配置。绝大多数代码解释器不需要SYS_ADMINNET_ADMINSYS_PTRACEDAC_OVERRIDE。十一、路径规范化用户提交输出名../../tenant-b/report.csv服务器端必须规范化publicPathresolveSafe(PathworkspaceRoot,StringuserName){PathcandidateworkspaceRoot.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 →上传隔离对象存储 →审核 →发布拒绝SocketFIFOBlock DeviceCharacter Device稀疏超大文件未知文件系统对象。只收集普通文件。十四、文件数量与大小配额攻击代码可能生成100万个小文件或创建一个超大稀疏文件。限制最大文件数单文件大小Workspace总大小Inode输出数量压缩后和解压后大小运行时间。十五、压缩炸弹上传ZIP看起来10MB解压后可能数百GB。验证总展开大小 文件数量 递归层级 压缩比 单文件大小 路径解压必须在隔离环境中进行。十六、对象存储Key与权限错误bucket/{filename}正确tenant/{tenantId}/run/{runId}/ workspace/{workspaceId}/artifact/{artifactId}但前缀仍不是权限本身。访问必须由IAM和应用授权控制。建议沙箱不能直接访问整个Bucket输入使用短期只读预签名URL或代理流输出只能写指定KeyListing默认禁止跨租户Key不可访问KMS按租户或敏感级别加密所有访问审计。十七、不要注入长期云凭证危险AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY不可信代码可以读取环境变量并外传。应使用短期令牌单一对象权限最小有效期网络限制代理服务Workload Identity并进一步缩权。十八、Artifact RegistrypublicrecordArtifactRecord(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 BytesMIME扩展名内容策略扫描结果。二十三、生成HTML与SVG的风险Agent输出HTML可能包含Script外链资源表单自动跳转恶意SVG浏览器漏洞利用。展示前下载而非内联Content-Disposition: attachment使用独立域名配置CSPHTML净化禁止Cookie禁止与主站同源。二十四、高风险文件格式高风险格式包括PickleJoblib不可信NotebookOffice宏可执行脚本带外部引用文件。不要在主服务中反序列化不可信对象。二十五、日志和错误页泄露日志可能暴露workspace/data/tenant-a/contract-123或记录完整命令和文件内容。日志应使用workspace_id artifact_id relative_path_hash避免绝对宿主机路径和敏感文件名。用户错误页也不应暴露容器IDBucketSecret路径内核信息内部服务地址。详细信息只进入受控Trace。二十六、网络外传即使文件系统隔离不可信代码仍可通过网络上传文件。默认策略无网络需要网络时域名AllowlistDNS代理禁止私网地址禁止云Metadata限制端口限制流量TLS策略请求审计。二十七、SSRF保护禁止访问127.0.0.0/8RFC1918私网Link-local云MetadataKubernetes Service网络管理平面数据库。同时防止DNS Rebinding、IPv6绕过、重定向到私网和混淆IP表示。二十八、进程与资源限制设置PID上限CPU内存Wall Time文件描述符子进程数线程磁盘IO网络带宽。防止Fork Bomb、资源耗尽和无限输出。二十九、清理不是删除目录这么简单清理对象包括容器或MicroVMWorkspace临时卷对象存储Token网络策略租约进程下载令牌缓存。三十、清理状态机publicrecordCleanupJob(StringcleanupId,StringworkspaceId,CleanupStatusstatus,intattempt,ListStringremainingResources,InstantnextAttemptAt,StringlastError){}清理失败不能把Workspace直接标记删除CLEANUP_FAILED →隔离 →告警 →重试 →人工处置在完成清理前不得复用运行时。三十一、TTL与业务保留不同数据需要不同策略临时脚本任务后立即删除 原始上传按用户政策 输出Artifact按产品保留 安全隔离样本按事故政策 审计Hash长期不要统一一个TTL。三十二、用户删除请求必须传播到Artifact Registry对象存储Checkpoint引用Replay包缓存搜索索引备份策略。业务审计可以保留最小Hash和删除事件。三十三、沙箱镜像使用最小基础镜像固定DigestSBOM漏洞扫描只安装必要包定期更新禁止运行时任意安装系统包。动态Python包安装应经过内部代理、Allowlist和缓存仓库。三十四、依赖供应链用户代码执行pipinstallsome-package可能拉取恶意包。策略默认禁止联网安装内部镜像仓库包AllowlistHash固定版本锁定安装发生在隔离构建层结果不复用到其他租户。三十五、租户隔离等级低风险可信内部代码容器独立Workspace中风险用户脚本加固容器严格网络独立节点池高风险公开代码MicroVM或专用远程执行池极高风险不提供执行 或人工离线环境三十六、运行ManifestpublicrecordSandboxExecutionManifest(StringexecutionId,StringtenantId,StringworkspaceId,StringimageDigest,StringisolationProfile,StringnetworkPolicyVersion,StringfilesystemPolicyVersion,ResourceLimitslimits,ListArtifactRefinputs,InstantstartedAt){}三十七、Tracesandbox.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 服务端下载授权 网络外传防护 可恢复清理状态机真正的安全目标不是“任务结束后大概率删掉文件”而是确保任何任务在执行、恢复、下载和清理的每个阶段都无法读取、覆盖或发布不属于自己的数据。