上传接口只允许图片,安全测试也确认文件内容是JPEG。几天后,同一对象通过一个带响应参数的URL返回了text/html,浏览器开始解释其中可执行内容。上传那一刻的校验没有失败,问题发生在文件被取回和分发时。
对象存储常支持通过查询参数覆盖Content-Type、Content-Disposition等响应头,便于下载和预览。若权限、签名或CDN缓存配置不当,这种便利会改变浏览器对文件的处理方式,形成存储型XSS或内容嗅探风险。
攻击链的关键不是文件扩展名
浏览器主要根据响应Content-Type和上下文决定如何处理内容。攻击者上传可控文件后,再通过response-content-type等参数要求存储服务以HTML返回;如果CDN把该变体缓存到公开地址,其他用户访问时可能执行脚本。
即使文件名是.jpg,只要响应被解释为HTML且位于可信站点域名下,危害仍然存在。反过来,正确的类型、下载处置和隔离域名可以显著降低风险。

为什么上传校验仍然会失守
上传校验通常检查扩展名、魔数、大小和病毒,但很少验证所有“取回方式”。对象存储的预签名URL、响应头覆盖、图片处理参数和CDN规则可能在之后改变结果。安全边界因此从上传接口延伸到存储API与分发层。
另一个误区是相信用户提交的Content-Type。服务端应根据文件内容重新识别,并把允许类型映射到固定响应头,而不是原样保存客户端声明。
正常响应与攻击响应如何区分
正常图片响应应返回明确的image/jpeg、image/png或image/webp,并设置X-Content-Type-Options: nosniff。需要下载的未知文件使用application/octet-stream和attachment。攻击响应常出现可疑的text/html覆盖参数、内联处置和缓存键遗漏。
审计时检查CDN是否把响应覆盖参数纳入缓存键。如果未纳入,攻击者生成的HTML响应可能污染不带参数的正常对象;若纳入,也要防止公开分享恶意变体。

一套完整的防御流程
上传阶段执行扩展名允许列表、内容检测、重编码和恶意文件扫描;存储阶段使用随机对象名、私有权限和最小化签名;取回阶段禁止客户端任意覆盖可执行Content-Type;分发阶段校验缓存键并添加nosniff与合理CSP。
用户上传内容最好使用与主站Cookie隔离的独立域名,且该域名不承载敏感会话。即便内容被错误解释,攻击者也更难读取主站身份数据。
发现问题后如何清理
立即禁用危险响应参数或限制签名权限,清理所有相关缓存变体,搜索访问日志中的覆盖参数。对已公开的恶意对象执行隔离并保留样本,评估是否有用户访问和会话风险。
随后检查相同存储桶、其他域名和图片处理服务,因为同一权限模型可能被多处复用。把“不同取回参数必须保持安全响应类型”加入自动化测试。

落地检查清单
- 服务端重新识别文件类型而非信任客户端声明
- 禁止或严格签名Content-Type覆盖参数
- CDN缓存键正确处理响应覆盖参数
- 添加nosniff并对未知文件强制下载
- 用户上传内容使用独立无Cookie域名
结语
对象安全不能停在上传成功那一刻。文件如何被签名、取回、改写响应头和进入缓存,决定了浏览器最终看到什么。把交付路径纳入文件安全模型,才能真正阻断这类存储型XSS。