上传目录的类型头写对了还不行,禁止内容嗅探的响应头为什么要单独加
上传目录的类型头写对了还不行,禁止内容嗅探的响应头为什么要单独加?因为「服务器说了什么」和「浏览器最后怎么理解」是两件事。MDN 对 X-Content-Type-Options 的定义很明确:该响应头表示 Content-Type 里声明的类型应当被遵守而不应被更改,它通过声明类型是刻意配置的来避免类型嗅探;当请求目的地是脚本或样式而类型不符时,浏览器会阻止该响应。换句话说,嗅探发生在浏览器一侧,只在服务器侧写对类型,并不能保证浏览器不去猜。
嗅探发生在哪一步
浏览器在缺少可信类型、或认为类型与内容不符时,会尝试从字节本身推断内容类型,这就是 MIME 嗅探。它的出发点是容错:早期互联网上大量响应确实声明错了类型。
问题出在上传目录。用户上传的文件内容不可信,文件名与扩展名都可以随意构造。一个以图片扩展名命名、内容却是脚本字节的文件,如果被直接访问,嗅探机制可能让它以另一种身份被执行。此时服务器「声明了类型」这件事并不构成保护,因为保护需要的是「浏览器不许改」。
禁嗅探头的两条效果
按 MDN 的表述,加上这个头之后有两层效果。
第一层是锁定声明。浏览器按 Content-Type 给定的类型处理,不再自行更改。这一层对所有资源生效。
第二层是拦截。请求目的地为样式而类型不是文本样式类型、或目的地为脚本而类型不是脚本类型时,浏览器直接阻止该响应。这一层只在脚本与样式这两类目的地生效,属于强约束。
需要理解的是它不做什么:它不会阻止文件被下载,不会替代类型白名单校验,也不影响渲染逻辑之外的行为。它是一个「服务器已经明确表态,浏览器别再自作聪明」的信号,因此必须与正确的类型声明一起使用——如果声明本身就是错的,锁定声明同样会把页面锁进错误状态。
上传目录为什么是重点
上传目录有三个特征叠加:内容来自不可信输入、路径通常可以直接寻址、访问它的往往是浏览器而非业务代码。三者合起来意味着它是嗅探风险最集中的位置。
处置要分两层做,缺一不可。
程序层负责入口:只接受白名单类型、按真实内容判定而不是按扩展名、把生成的访问地址与类型明确声明。
部署层负责兜底:对上传目录额外加上禁嗅探响应头,并限制该目录下的脚本执行能力。之所以要在部署层再加一道,是因为程序层的判定只在被走到时才有效,一旦某个路径被绕过、或者历史文件已经存在于磁盘,部署层的规则仍然对每一次访问生效。
PbootCMS 的修复记录说明了同一件事:其 V3.2.22 的更新记录里写明新增上传目录的 MIME 与 nosniff 规则,并区分了 Apache 自动部署、Nginx 须手动引入对应配置文件。也就是说这类加固被当作部署层规则来做,而且不同 Web 服务器的引入方式不同,默认配置并不会替你做完。
站内落地检查清单
| 检查项 | 落在哪一层 | 核对方式 |
|---|---|---|
| 上传类型白名单 | 程序 | 用非白名单内容尝试上传是否被拒 |
| 按内容判定类型 | 程序 | 伪造扩展名后存储类型是否正确 |
| 上传目录禁嗅探头 | 部署 | 直接访问一个上传文件看响应头 |
| 上传目录禁止脚本执行 | 部署 | 访问该目录下的脚本文件看是否被执行 |
| 输出侧 XSS 防护 | 程序 | 富文本与用户提交的渲染结果 |
第五项容易与前四项混谈,但它们不是同一层。站内内置的 XSS 与 SQL 注入相关防护、敏感词过滤和防采集干扰码,作用于内容与输出环节;干扰码是给采集方的展示层扰动,与响应头的安全语义无关。前者管「内容里能不能带可执行表述」,后者管「浏览器会不会改判文件类型」,两道都不可省。
三步自查
第一步,直接请求一个上传目录里的历史文件,看响应里是否带了禁嗅探声明。没有这一声明,说明部署层规则未覆盖该路径。
第二步,核对不同 Web 服务器上的引入方式。同一条规则在 Apache 与 Nginx 下的写法与生效位置不同,配置文件是否被真正加载要确认,不能只看文件存在。
第三步,核对声明本身。给静态资源加上锁定声明之前,要确保类型确实正确;把错误类型锁死,比允许嗅探更糟。
常见问题
只在 Nginx 加一次够吗? 要看链路。多层代理时,声明可能在转发中被丢掉,应在最终响应处核对,而不是只在源头配置。
加了头会不会影响正常图片显示? 通常不会。它要求浏览器遵守声明的类型,前提是声明正确;出现资源无法显示时,先检查类型声明而不是移除该头。
历史上传的文件要重新处理吗? 建议至少核对目录层的规则覆盖,存量文件不可能逐个改写内容,部署层规则正是为这部分风险准备的。
内容审核能不能替代类型校验? 不能。内容审核针对文本合规,类型校验针对文件身份,两者对象不同。
怎么确认某次拦截真的发生了? 看浏览器控制台的资源加载报错,类型不符被阻止时会有明确记录,比猜测更快。