接收 multipart 请求并把字节写进对象存储,只完成了传输。客户端可以伪造扩展名和 Content-Type,图片可能携带脚本或解析器漏洞,几十 KB 的压缩包可能展开成数 GB,原始文件名还可能变成路径穿越入口。若上传成功后立即返回公开 URL,所有后续检查都已经太晚。

安全上传应是一条有状态的内容处理流水线:限制进入系统的成本,把未知字节放进隔离区,验证和转换后才发布,并在下载时继续执行授权与响应头策略。

用状态机隔开“收到”和“可用”

RECEIVING -> QUARANTINED -> SCANNING -> READY
                                  \-> REJECTED

上传接口先创建带所有者、租户、预期用途和大小上限的 upload session。字节接收完成后返回 202 Accepted 或一个 QUARANTINED 资源,而不是业务可访问地址。扫描 worker 通过幂等任务把状态推进到 READY 或 REJECTED;客户端查询状态或接收事件通知。

在最外层限制成本

网关、应用和对象存储都要设置相互一致的请求大小、读取时间和并发上限。只在业务代码读完整个 body 后检查大小,攻击流量已经占用了带宽、内存和临时磁盘。分块上传还需限制分块数、总大小、session 寿命和每个主体的未完成上传配额。

上传前先做身份与用途授权,例如头像只能属于当前用户,合同附件只能写入获准项目。

校验和只能发现传输损坏。服务端流式计算摘要,并在 finalize 时核对大小、所有者和 session 状态。

文件名只是展示元数据

永远不要用原始文件名作为磁盘路径或对象 key。存储标识由服务端生成,例如随机 UUID;原名经过 Unicode 规范化、长度与控制字符检查后,只作为数据库中的展示字段。去掉客户端路径片段,拒绝空名、保留名、尾随点、斜杠、反斜杠和 NUL 等危险输入。

扩展名采用业务允许列表,并在完整解码和规范化后检查。重命名能隔离路径风险,但不会改变内容类型。

类型判断需要多项证据

请求里的 Content-Type 是客户端声明,可以伪造。扩展名、声明类型、文件签名字节和实际解析结果应相互匹配;任何矛盾都拒绝或进入人工流程。文件签名也不是完整证明:多格式 polyglot 可能同时满足两个解析器,恶意内容还可能拥有合法文件头。

只允许业务需要的类型,并用专用库严格解析。图片可解码后重新编码并移除元数据;发布转换后的派生对象,原件留在隔离区或按策略删除。

压缩包有独立资源预算

如果产品不需要解包,就不要解包。必须处理 archive 时,同时限制条目数、递归深度、单项与总展开字节、压缩比、文件名长度和处理时间。每个条目都要重复类型校验。

规范化目标路径并确认它仍在临时根目录,拒绝绝对路径、..、设备文件和链接。先检查清单再流式展开,超出预算立即终止并清理。

隔离区不能被当成下载区

隔离对象应在私有 bucket、独立账户或至少独立前缀中,应用服务器不执行其中内容,也不通过静态 Web 根目录暴露。直传对象存储时,预签名能力只允许特定主体向一个随机 key 执行限定操作,并设置短有效期;finalize 仍由服务端重新验证对象,而不是相信“上传请求成功”。

恶意软件扫描、解析器和转码器应在低权限、有限 CPU/内存/磁盘/网络的沙箱中运行。扫描服务不可用时保持 SCANNING 或失败关闭,不能为了可用性自动放行。更新扫描引擎或策略后,应能找出旧版本通过的 READY 对象并按风险重新扫描。

下载是第二个安全边界

每次下载重新检查当前用户对文件记录的权限,不把不可猜 URL 当成授权。默认从与主站隔离的下载域名返回已知 Content-Type、X-Content-Type-Options: nosniff,以及 RFC 6266 的 Content-Disposition: attachment。服务端生成安全的 filename/filename*,不把原名直接拼进响应头。

只有明确允许内联预览的安全派生类型才使用 inline,并配合受限 CSP。用户上传的 HTML、SVG 等主动内容不应与主站共享 origin;否则它可能读取同源数据或执行带用户身份的请求。

生命周期也是安全控制

定时清理未完成 session、失败分块、REJECTED 原件和孤儿对象。清理前按数据库所有权核对,避免删除仍被引用的对象;审计只记录主体、策略结论和对象版本。

测试样本至少覆盖伪造 MIME、双扩展名、路径穿越、Unicode 与控制字符、零字节和超大流、polyglot、损坏解析结构、压缩炸弹、递归 archive、扫描超时、重复 finalize、回调乱序、隔离对象越权下载和响应头注入。扫描器测试使用专用无害测试标记,不能把真实恶意样本混入普通测试环境。

文件上传的完成条件不是字节已经落盘,而是内容成本受限、类型得到验证、处理过程被隔离、发布状态可追溯,且下载仍遵守授权和浏览器安全边界。把这些步骤写成状态机后,上传功能才不会变成绕过应用防线的任意内容入口。