网络表单文件验证实现检查清单
分类:安全·实现
本文目前仅提供日文版本。我们正在进行翻译工作。
文件上传功能的实现涉及众多安全考虑,也存在许多容易被忽视的陷阱。本文解释了在生产环境中安全实现文件上传所需的检查清单。
清单概述
此清单主要关注<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> |
总结
实现安全的文件上传需要在多个层级进行验证。特别是,必须实现以下三点。
- <strong>服务器端 MIME 类型验证</strong>(<code>finfo</code> 使用)— 不信任客户端申报
- <strong>使用随机文件名保存</strong> — 不使用原始文件名
- <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>