Skip to content

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)) {
    // 处理图片
}

参考链接