ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Code Interpreter临时文件为什么泄露到其他任务?工作目录、对象存储与沙箱生命周期完整排查

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 +服务端下载授权 +网络外传防护 +可恢复清理状态机

真正的安全目标不是“任务结束后大概率删掉文件”,而是确保任何任务在执行、恢复、下载和清理的每个阶段,都无法读取、覆盖或发布不属于自己的数据。

返回列表