Fileinfo 扩展
概述
PHP 的 Fileinfo 扩展(ext-fileinfo)提供了检测文件 MIME 类型的能力,比依赖文件扩展名更可靠。它通过读取文件内容头来判断实际类型,能有效防止伪造扩展名的攻击。finfo_open()、finfo_file()、finfo_buffer() 是核心函数。
适用场景
- 文件上传类型验证
- 下载时设置正确的 Content-Type
- 文件分类和管理
- 安全验证文件实际类型
基础概念
核心函数
| 函数 | 功能 | 返回值 |
|---|---|---|
finfo_open() | 创建 Fileinfo 资源 | resource|false |
finfo_file() | 检测文件 MIME 类型 | string|false |
finfo_buffer() | 检测字符串的 MIME 类型 | string|false |
finfo_close() | 关闭 Fileinfo 资源 | bool |
finfo_set_flags() | 设置 Fileinfo 标志 | bool |
mime_content_type() | 获取 MIME 类型(已弃用) | string|false |
Fileinfo 标志常量
| 标志 | 说明 |
|---|---|
FILEINFO_MIME_TYPE | 仅返回 MIME 类型 |
FILEINFO_MIME_ENCODING | 仅返回编码 |
FILEINFO_MIME | 返回 MIME 类型和编码(默认) |
FILEINFO_NONE | 无特殊处理 |
mime_content_type 已弃用
mime_content_type() 在 PHP 7.4 中弃用,PHP 8.0 中移除。请使用 finfo_file() 替代。
语法与代码示例
基本 MIME 类型检测
php
<?php
// 创建 Fileinfo 实例
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo === false) {
throw new RuntimeException('无法初始化 Fileinfo');
}
// 检测文件 MIME 类型
$mime = finfo_file($finfo, '/tmp/photo.jpg');
echo "MIME 类型: {$mime}\n"; // image/jpeg
$mime = finfo_file($finfo, '/tmp/document.pdf');
echo "MIME 类型: {$mime}\n"; // application/pdf
$mime = finfo_file($finfo, '/tmp/data.json');
echo "MIME 类型: {$mime}\n"; // application/json
$mime = finfo_file($finfo, '/tmp/archive.zip');
echo "MIME 类型: {$mime}\n"; // application/zip 或 application/x-zip-compressed
finfo_close($finfo);仅获取 MIME 类型
php
<?php
// FILEINFO_MIME_TYPE 只返回 MIME 类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, '/tmp/photo.png');
echo $mime . PHP_EOL; // image/png
finfo_close($finfo);检测字符串的 MIME 类型
php
<?php
$finfo = finfo_open(FILEINFO_MIME_TYPE);
// 从二进制数据中检测 MIME 类型
$imageData = file_get_contents('/tmp/photo.jpg');
$mime = finfo_buffer($finfo, $imageData);
echo $mime . PHP_EOL; // image/jpeg
// 检测 base64 编码数据
$base64Data = base64_encode($imageData);
$mime = finfo_buffer($finfo, base64_decode($base64Data));
echo $mime . PHP_EOL; // image/jpeg
// 检测字符串数据
$mime = finfo_buffer($finfo, '<?php echo "hello";');
echo $mime . PHP_EOL; // text/x-php
finfo_close($finfo);使用魔术数据库文件
php
<?php
// 指定魔术数据库文件路径
$magicFile = '/usr/share/misc/magic'; // Linux
// $magicFile = '/usr/share/file/magic.mime'; // macOS
// $magicFile = 'C:\PHP\extras\magic'; // Windows
$finfo = finfo_open(FILEINFO_MIME_TYPE, $magicFile);
if ($finfo === false) {
throw new RuntimeException('无法加载魔术数据库');
}
$mime = finfo_file($finfo, '/tmp/document.pdf');
echo $mime . PHP_EOL;
finfo_close($finfo);使用 finfo 类(面向对象)
php
<?php
// PHP 8.0+ 推荐使用 finfo 类
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file('/tmp/image.png');
echo $mime . PHP_EOL; // image/png
// 检测 buffer
$mime = $finfo->buffer(file_get_contents('/tmp/image.png'));
echo $mime . PHP_EOL; // image/png
// 设置标志
$finfo->setFlags(FILEINFO_MIME_TYPE);实战示例
文件上传 MIME 类型验证
php
<?php
declare(strict_types=1);
class UploadValidator
{
/**
* 允许的 MIME 类型白名单
* @var array<string, string[]>
*/
private array $allowedTypes = [
'image' => [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/svg+xml',
],
'document' => [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
];
private finfo $finfo;
public function __construct()
{
$this->finfo = new finfo(FILEINFO_MIME_TYPE);
}
/**
* 验证上传文件的 MIME 类型
*/
public function validateMime(string $filePath, array $allowedMimes): bool
{
if (!file_exists($filePath)) {
return false;
}
$mime = $this->finfo->file($filePath);
return in_array($mime, $allowedMimes, true);
}
/**
* 按分类验证
*/
public function validateByCategory(string $filePath, string $category): bool
{
if (!isset($this->allowedTypes[$category])) {
throw new InvalidArgumentException("未知分类: {$category}");
}
return $this->validateMime($filePath, $this->allowedTypes[$category]);
}
/**
* 获取文件 MIME 类型
*/
public function getMimeType(string $filePath): string
{
$mime = $this->finfo->file($filePath);
return $mime ?: 'application/octet-stream';
}
}
// 使用示例
$validator = new UploadValidator();
// 验证图片
if ($validator->validateByCategory('/tmp/upload.jpg', 'image')) {
echo "图片类型验证通过\n";
}
// 自定义验证
if ($validator->validateMime('/tmp/file.pdf', ['application/pdf'])) {
echo "PDF 文件验证通过\n";
}文件类型判断工具
php
<?php
declare(strict_types=1);
class FileTypeDetector
{
private finfo $finfo;
public function __construct()
{
$this->finfo = new finfo(FILEINFO_MIME);
}
/**
* 获取完整 MIME 信息
*/
public function analyze(string $filePath): array
{
if (!file_exists($filePath)) {
throw new RuntimeException("文件不存在: {$filePath}");
}
$mimeInfo = $this->finfo->file($filePath);
return [
'path' => $filePath,
'mime' => $mimeInfo,
'type' => $this->getType($filePath),
'category' => $this->getCategory($filePath),
'size' => filesize($filePath),
'extension' => pathinfo($filePath, PATHINFO_EXTENSION),
];
}
/**
* 判断文件大类
*/
public function getType(string $filePath): string
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($filePath);
if (str_starts_with($mime, 'image/')) {
return 'image';
}
if (str_starts_with($mime, 'video/')) {
return 'video';
}
if (str_starts_with($mime, 'audio/')) {
return 'audio';
}
if (str_starts_with($mime, 'text/') || str_contains($mime, 'xml') || str_contains($mime, 'json')) {
return 'text';
}
if (str_contains($mime, 'zip') || str_contains($mime, 'tar') || str_contains($mime, 'gzip')) {
return 'archive';
}
return 'binary';
}
/**
* 获取分类
*/
public function getCategory(string $filePath): string
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($filePath);
$map = [
'image/jpeg' => 'JPEG 图片',
'image/png' => 'PNG 图片',
'image/gif' => 'GIF 图片',
'image/webp' => 'WebP 图片',
'image/svg+xml' => 'SVG 矢量图',
'application/pdf' => 'PDF 文档',
'application/json' => 'JSON 数据',
'text/plain' => '纯文本',
'text/html' => 'HTML 页面',
'text/css' => 'CSS 样式表',
'application/javascript' => 'JavaScript',
'application/zip' => 'ZIP 压缩包',
'application/gzip' => 'GZIP 压缩文件',
'application/x-tar' => 'TAR 归档',
];
return $map[$mime] ?? $mime;
}
/**
* 检测扩展名与实际 MIME 是否匹配
*/
public function extensionMatches(string $filePath): bool
{
$mime = $this->finfo->file($filePath);
$extMap = [
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'gif' => ['image/gif'],
'pdf' => ['application/pdf'],
'zip' => ['application/zip', 'application/x-zip-compressed'],
'json' => ['application/json'],
];
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if (!isset($extMap[$ext])) {
return true; // 未知扩展名不做验证
}
return in_array($mime, $extMap[$ext], true);
}
}
// 使用示例
$detector = new FileTypeDetector();
$info = $detector->analyze('/tmp/photo.jpg');
print_r($info);
echo "文件类型: " . $detector->getType('/tmp/video.mp4') . PHP_EOL;
echo "分类: " . $detector->getCategory('/tmp/doc.pdf') . PHP_EOL;
echo "扩展名匹配: " . ($detector->extensionMatches('/tmp/fake.jpg') ? '是' : '否') . PHP_EOL;注意事项
扩展名与 MIME 的区别
php
<?php
// 文件扩展名可以被随意修改
copy('/tmp/malware.exe', '/tmp/cute_cat.jpg');
// pathinfo 只看扩展名
echo pathinfo('/tmp/cute_cat.jpg', PATHINFO_EXTENSION); // 'jpg'
// finfo_file 检查文件内容
$finfo = new finfo(FILEINFO_MIME_TYPE);
echo $finfo->file('/tmp/cute_cat.jpg'); // 'application/x-dosexec'(真实类型)
// 安全验证必须使用 finfo_file安全警告
永远不要仅依赖文件扩展名来判断文件类型。攻击者可以将恶意文件伪装为图片等安全类型上传。必须使用 finfo_file() 验证文件实际内容。
Fileinfo 扩展安装
bash
# Ubuntu/Debian
sudo apt-get install php-fileinfo
# CentOS/RHEL
sudo yum install php-fileinfo
# macOS (Homebrew PHP)
brew install php
# 编译安装
# PHP 编译时添加 --enable-fileinfo
# 或安装后添加 extension=fileinfo.so 到 php.ini
# 检查是否已安装
php -m | grep fileinfo性能考虑
php
<?php
// finfo_open 只需调用一次
// 不好:每次检测都创建实例
foreach ($files as $file) {
$finfo = new finfo(FILEINFO_MIME_TYPE); // 重复创建
$mime = $finfo->file($file);
}
// 好:复用实例
$finfo = new finfo(FILEINFO_MIME_TYPE);
foreach ($files as $file) {
$mime = $finfo->file($file);
}最佳实践
1. 文件上传双重验证
php
<?php
function validateUpload(string $tmpPath, string $originalName, array $allowedExts, array $allowedMimes): bool
{
// 验证扩展名
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts, true)) {
return false;
}
// 验证 MIME 类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpPath);
if (!in_array($mime, $allowedMimes, true)) {
return false;
}
return true;
}
// 验证图片上传
$isValid = validateUpload(
$_FILES['avatar']['tmp_name'],
$_FILES['avatar']['name'],
['jpg', 'jpeg', 'png', 'gif', 'webp'],
['image/jpeg', 'image/png', 'image/gif', 'image/webp']
);2. 设置下载文件的 Content-Type
php
<?php
function downloadFile(string $filePath): void
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($filePath);
$filename = basename($filePath);
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . filesize($filePath));
header('Cache-Control: no-cache');
readfile($filePath);
exit;
}3. 使用类型常量
php
<?php
class MimeTypes
{
public const JPEG = 'image/jpeg';
public const PNG = 'image/png';
public const GIF = 'image/gif';
public const WEBP = 'image/webp';
public const PDF = 'application/pdf';
public const JSON = 'application/json';
public const ZIP = 'application/zip';
public const IMAGE_TYPES = [self::JPEG, self::PNG, self::GIF, self::WEBP];
public const DOC_TYPES = [self::PDF];
}
// 使用
if (in_array($mime, MimeTypes::IMAGE_TYPES, true)) {
// 处理图片
}