文件类型如何严格校验

wen 开源项目 24

本文目录导读:

文件类型如何严格校验

  1. 第一层:客户端(前端)校验——仅用于用户体验
  2. 第二层:服务器端校验——真正的防线开始
  3. 第三层:深度内容与结构校验——最严格模式
  4. 第四层:其他关键措施
  5. 严格校验流程

文件类型严格校验的核心原则是永远不要信任文件扩展名,因为攻击者可以轻易地将恶意文件(如 .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
  • 代码(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-parserpoppler 检查PDF结构,扫描JavaScript或恶意对象。
    • Office文档:解压后检查XML内容,查找宏(VBA)。

第四层:其他关键措施

黑名单与白名单

  • 绝对不要用黑名单(如禁止 .exe, .php),攻击者总有未知后缀名可以绕过。
  • 必须使用白名单:只允许明确需要的类型(如 ['image/jpeg', 'image/png'])。

文件扩展名与内容的双重校验

将上述第二层的魔数校验结果与文件扩展名进行强一致匹配

  1. 用户上传 cat.jpg
  2. 读取魔数,得到 image/png -> 失败,因为扩展名是jpg但内容是png。
  3. 或者,如果允许上传图片,发现魔数是 application/zip -> 失败。

禁止执行权限与隔离存储

  • 上传的文件不要存储在Web根目录下。
  • 存储在专门的服务器或云存储(如AWS S3,阿里云OSS),并设置为禁止执行脚本禁用目录列表
  • 重命名文件,去掉原有扩展名或使用UUID作为文件名(如 a1b2c3d4.dat),完全由后端逻辑根据魔数来确定返回时的Content-Type。

严格校验流程

  1. 前端:初步过滤,提升用户体验(可绕过)。
  2. 后端接收:检查文件名后缀(白名单)。
  3. 后端深入:读取文件头部(Magic Number),判断真实内容类型。
    • 如果允许图片,检查是否为 FF D8 FF, 89 50 4E 47 等,拒绝 PK4D 5A
  4. 后端强化:使用专业库重新解析/渲染文件(如重编码图片),踢出附加数据。
  5. 存储:使用UUID重命名文件,去除用户原扩展名,存储在非Web目录或对象存储中。

一句话总结:只在服务器端用魔数(Magic Number)校验文件的二进制头,搭配白名单策略,并对图片类文件进行重编码,才能做到最严格的文件类型校验。

抱歉,评论功能暂时关闭!