MIME 嗅探与 X-Content-Type-Options:nosniff 详解
即使服务器发送了错误的 Content-Type,浏览器也可以通过「嗅探」响应的实际内容来决定如何处理它。X-Content-Type-Options: nosniff 响应头告诉浏览器信任声明的类型,并拒绝将文件(如上传的图片)提升为可执行脚本。
这是什么
MIME 嗅探是浏览器的一种启发式行为,当服务器的 Content-Type 头缺失或与内容不匹配时,它会检查响应的前几个字节来推断内容类型。例如,一个声明为 text/plain 但以 <script> 开头的文件,可能会被重新分类并作为 HTML 执行。
X-Content-Type-Options 是一个单值响应头:X-Content-Type-Options: nosniff。当该头存在时,浏览器会拒绝将响应提升为不同的 MIME 类型——以 application/octet-stream 提供的上传文件将保持二进制状态,不会变成可执行的脚本或样式表。
为什么重要
用户生成的内容是主要风险。如果你的应用允许文件上传并从与应用相同的源提供这些文件,一个伪装成图片的恶意 HTML 文件在被嗅探时可能在受害者的浏览器中执行——即使你在服务器端验证了文件扩展名,这也是一种通过文件上传实现的存储型 XSS。
没有 nosniff,浏览器还可能执行从意外来源获取的样式表。当样式表被当作脚本执行时,结合其他配置错误会产生跨源数据泄露路径。nosniff 是一种低成本的全局安全措施,能加固应用的每一个内容边界。
如何修复
在 next.config.js 中设置 X-Content-Type-Options: nosniff,使其适用于所有响应,包括 API 路由和静态文件路由。
// next.config.js
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
],
},
];
},
};在 server 块中添加指令,使其适用于所有通过 nginx 代理的响应。
add_header X-Content-Type-Options "nosniff" always;当每个响应都带有正确的 Content-Type 头时,nosniff 最为有效。对于用户上传的文件,应使用适当的类型提供,并从独立的源或存储域提供——而非与应用相同的源——这样即使文件被执行,也无法访问用户的会话 Cookie。
扩展名检查可以被绕过。使用 file-type(npm)等库在服务器端检查文件的魔术字节,在文件到达存储层之前拒绝任何不在安全类型允许列表中的文件。
import { fileTypeFromBuffer } from "file-type";
const ALLOWED = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
async function validateUpload(buffer: Buffer): Promise<void> {
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED.has(type.mime)) {
throw new Error("Upload rejected: unsupported file type");
}
}常见问题
AppSafe 对缺少 X-Content-Type-Options 头的报告严重程度是多少?
中等。该头是一项低摩擦的安全加固措施——仅需一行配置——因此省略它是一个明显的缺口,但主动利用还需要独立的上传或注入向量。
nosniff 会影响正常的页面加载吗?
实际上不会。nosniff 只阻止提升到更危险的类型。你的 HTML、CSS 和 JavaScript 已经使用正确的类型提供,因此会正常加载。
现代浏览器已经更智能了,X-Content-Type-Options 还有必要吗?
有。不同浏览器引擎和版本的嗅探行为各不相同,该响应头是唯一可移植的保证。所有现代浏览器(Chrome、Firefox、Safari、Edge)都支持 nosniff。