跳到内容

网络表单文件验证实现检查清单

分类:安全·实现

文件上传功能的实现涉及众多安全考虑,也存在许多容易被忽视的陷阱。本文解释了在生产环境中安全实现文件上传所需的检查清单。

文件验证推荐顺序 安全顺序: 从轻量到重量级检验 1. 文件大小 bytes <= max NG → 立即拒绝 2. 扩展名 .jpg / .png ... NG → 立即拒绝 3. MIME 类型 finfo / mime_content_type NG → 立即拒绝 4. 魔术字节 FF D8 FF (JPEG) ... NG → 立即拒绝 5. 内容扫描 ClamAV / VirusTotal NG → 立即拒绝
图1: 先执行轻量验证并及早拒绝

清单概述

此清单主要关注后端(服务器端)验证。前端验证作为辅助的用户体验改进而实现,但不提供安全保障。

1. 文件大小验证

  • 上传限制以字节为单位定义(不混淆 MB 和 MiB)
  • 对于 PHP,已配置 upload_max_filesizepost_max_size
  • 对于 Nginx,client_max_body_size 包括 multipart 开销的余量
  • $_FILES['file']['error'] 为 UPLOAD_ERR_INI_SIZE / UPLOAD_ERR_FORM_SIZE 时,有错误处理
  • 有最小文件大小检查(排除 0 字节文件)
 $maxBytes) {
        throw new \RuntimeException(sprintf(
            'ファイルサイズ(%s)が上限(%s)を超えています',
            number_format($file['size']),
            number_format($maxBytes)
        ));
    }
}

2. 文件格式验证(MIME 类型)

  • 不信任从客户端发送的 Content-Type$_FILES['file']['type']
  • 使用 finfo / mime_content_type() 执行服务器端 MIME 类型验证
  • 定义了允许的 MIME 类型的白名单
file($file['tmp_name']);

if (!in_array($mimeType, $allowed, true)) {
    throw new \RuntimeException('許可されていないファイル形式です: ' . $mimeType);
}

3. 文件扩展名验证

  • 定义了文件扩展名的白名单(白名单,而不是黑名单)
  • 双扩展名(例如 shell.php.jpg)被检测并拒绝
  • 在验证过程中应用大小写规范化(将 .JPG.jpg 视为相同)
 2) {
    throw new \RuntimeException('不正なファイル名です');
}

$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
    throw new \RuntimeException('許可されていない拡張子です: ' . $ext);
}

4. 魔数(文件签名)验证

  • 对关键文件(例如排除可执行文件)验证魔术字节

5. 保存目标和文件名的安全处理

  • 保存目录在 Web 根目录之外(或通过 XSendFile/X-Accel-Redirect 控制)
  • 保存的文件名是随机生成的(例如 UUID),而不是原始文件名
  • 路径遍历(例如 ../../../etc/passwd)通过验证被消除
  • 保存目录没有 PHP 执行权限(通过 .htaccess 或 Nginx 配置禁用 PHP 处理)

6. 错误处理和响应

  • 上传成功时返回适当的 HTTP 状态代码 (200/201)
  • 超出大小时返回 413 Payload Too Large
  • 对于无效的文件格式,返回 422 Unprocessable Entity
  • 错误消息不包含服务器内部信息(路径、版本等)

7. 测试用例

实现后,请运行以下测试用例以验证行为。您可以使用 DevLab 中可用的测试文件。

测试用例预期结果要使用的文件
恰好在限制大小的文件成功阈值文件
超过限制 1 字节的文件413错误阈值文件
0字节的空文件验证错误手动创建
扩展名被伪装的文件 (PHP 伪装成 .jpg)MIME 错误损坏文件
标头损坏文件验证错误损坏文件

总结

实现安全的文件上传需要在多个层级进行验证。特别是,必须实现以下三点。

  1. 服务器端 MIME 类型验证finfo 使用)— 不信任客户端申报
  2. 使用随机文件名保存 — 不使用原始文件名
  3. 禁用上传目录中的 PHP 执行——防止脚本在上传目录中执行

❓ 常见问题

文件上传校验最起码要做到哪些?
至少三点:在服务端用 finfo 校验 MIME 类型而非信任客户端申报;使用随机生成的文件名保存而非原文件名;在上传目录中禁用 PHP 执行,确保放入的文件无法运行。
为什么只检查扩展名不够?
扩展名极易伪造,无法识别 shell.php.jpg 这类双扩展名的恶意文件。应在扩展名白名单之外,结合 finfo 的 MIME 校验与文件头魔术字节校验。
文件上传应返回哪些 HTTP 状态码?
成功时返回 200 或 201,文件超出上限时返回 413 Payload Too Large,格式非法时返回 422 Unprocessable Entity。注意错误信息中不要包含服务器内部信息(路径、版本等)。