本文目录导读:

文件类型严格校验的核心原则是永远不要信任文件扩展名,因为攻击者可以轻易地将恶意文件(如 .exe)重命名为看似无害的 .jpg 或 .pdf。
严格校验文件类型通常需要采用多层次、纵深防御的策略,以下是实现严格校验的完整方案,从前端到后端,从轻量级到深度分析:
第一层:客户端(前端)校验——仅用于用户体验
注意: 客户端校验可以轻易被绕过,不能作为安全防线。
-
方法:检查文件扩展名。
-
代码(JS):
const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif']; const fileName = file.name; // 来自 <input type="file"> const extension = fileName.split('.').pop().toLowerCase(); if (!allowedExtensions.includes(extension)) { alert('不支持的文件类型'); return; } -
局限:修改扩展名即可绕过。
第二层:服务器端校验——真正的防线开始
检查 MIME Type(内容类型)
这是HTTP请求头中的 Content-Type,但同样可以被伪造。
- 方法:检查请求头的
Content-Type字段。 - 代码(Node.js + Multer):
const upload = multer({ fileFilter: (req, file, cb) => { // 检查 MIME 类型 const allowedMimes = ['image/jpeg', 'image/png', 'image/gif']; if (allowedMimes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Invalid MIME type'), false); } } }); - 局限:攻击者可以修改请求头,将
exe的 MIME 类型改为image/jpeg。
魔数(Magic Number)检查——核心硬核方案
这是最重要的一步。 每个文件类型的头部(Header)都有其特定的二进制签名(Magic Number),无论扩展名或MIME如何修改,文件的二进制内容不会变。
-
原理:读取文件的前几个字节,与已知的文件类型签名库进行比对。
-
常见签名(十六进制):
- JPEG:
FF D8 FF - PNG:
89 50 4E 47 - GIF:
47 49 46 38 - PDF:
25 50 44 46 - ZIP:
50 4B 03 04(如 Office docx/xlsx 实际上是ZIP) - EXE:
4D 5A
- JPEG:
-
代码(Python 示例):
import struct def validate_image(file_path): with open(file_path, 'rb') as f: header = f.read(8) # 读取前8个字节 # JPEG if header[0:3] == b'\xff\xd8\xff': return 'JPEG' # PNG elif header[0:8] == b'\x89PNG\r\n\x1a\n': return 'PNG' # GIF elif header[0:6] in [b'GIF87a', b'GIF89a']: return 'GIF' else: raise ValueError("文件类型不合法") -
代码(Node.js 示例:使用
file-type库):const { fileTypeFromBuffer } = require('file-type'); // 假设已经有了文件的 Buffer 数据 const buffer = fs.readFileSync(filePath); const type = await fileTypeFromBuffer(buffer); if (type && type.mime === 'image/jpeg') { console.log('是真正的JPEG'); } else { throw new Error('类型不匹配'); } -
注意:有些文件(如Word的
.docx)实际上是ZIP包,其魔数是PK,如果你只允许图片,需要确保它不是ZIP签名。
第三层:深度内容与结构校验——最严格模式
有些攻击者可以将真正的图片内容与恶意脚本拼接(如图片末尾附加PHP代码),仅检查文件头可能无法发现。
- 方法:使用专门的库解析文件结构,并尝试重新渲染或重编码。
- 应用场景:
- 图片:使用
getimagesize()(PHP),PIL(Python) 或Sharp(Node.js) 读取图片,如果文件损坏或包含额外数据,可能会报错,最彻底的方式是重新编码图片(解码后再编码),这会彻底剥离任何隐藏在图片元数据或结尾的恶意代码。 - PDF:使用
pdf-parser或poppler检查PDF结构,扫描JavaScript或恶意对象。 - Office文档:解压后检查XML内容,查找宏(VBA)。
- 图片:使用
第四层:其他关键措施
黑名单与白名单
- 绝对不要用黑名单(如禁止
.exe,.php),攻击者总有未知后缀名可以绕过。 - 必须使用白名单:只允许明确需要的类型(如
['image/jpeg', 'image/png'])。
文件扩展名与内容的双重校验
将上述第二层的魔数校验结果与文件扩展名进行强一致匹配。
- 用户上传
cat.jpg - 读取魔数,得到
image/png-> 失败,因为扩展名是jpg但内容是png。 - 或者,如果允许上传图片,发现魔数是
application/zip-> 失败。
禁止执行权限与隔离存储
- 上传的文件不要存储在Web根目录下。
- 存储在专门的服务器或云存储(如AWS S3,阿里云OSS),并设置为禁止执行脚本、禁用目录列表。
- 重命名文件,去掉原有扩展名或使用UUID作为文件名(如
a1b2c3d4.dat),完全由后端逻辑根据魔数来确定返回时的Content-Type。
严格校验流程
- 前端:初步过滤,提升用户体验(可绕过)。
- 后端接收:检查文件名后缀(白名单)。
- 后端深入:读取文件头部(Magic Number),判断真实内容类型。
- 如果允许图片,检查是否为
FF D8 FF,89 50 4E 47等,拒绝PK或4D 5A。
- 如果允许图片,检查是否为
- 后端强化:使用专业库重新解析/渲染文件(如重编码图片),踢出附加数据。
- 存储:使用UUID重命名文件,去除用户原扩展名,存储在非Web目录或对象存储中。
一句话总结:只在服务器端用魔数(Magic Number)校验文件的二进制头,搭配白名单策略,并对图片类文件进行重编码,才能做到最严格的文件类型校验。