跳到内容

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

分类:安全·实现
本文目前仅提供日文版本。我们正在进行翻译工作。

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

文件验证推荐顺序 安全顺序: 从轻量到重量级检验 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: 先执行轻量验证并及早拒绝

清单概述

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

1. 文件大小验证

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

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

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

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

3. 文件扩展名验证

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

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

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

  • <input type="checkbox" disabled> 对关键文件(例如排除可执行文件)验证魔术字节

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

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

6. 错误处理和响应

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

7. 测试用例

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

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

总结

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

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

本文中可用的测试文件

  • → <a href="/ja/files/broken/" class="text-primary-600 dark:text-primary-400 hover:underline">损坏文件列表(用于验证错误测试)</a>
  • → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">边界值测试文件列表(9.9MB / 10MB / 10.1MB)</a>
  • → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">图像测试文件列表 (PNG / JPG / WebP / GIF)</a>

相关文章

  • → <a href="/ja/blog/how-to-test-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">如何正确测试文件上传限制</a>
  • → <a href="/ja/reference/magic-bytes/" class="text-primary-600 dark:text-primary-400 hover:underline">魔数(文件签名)参考</a>