详解 WordPress `wp_check_filetype()` 函数的源码:如何通过文件头而非扩展名判断文件类型。

各位听众,早上好!我是今天的主讲人,很高兴能和大家一起探讨WordPress的wp_check_filetype()函数,并深入挖掘它是如何通过文件头(magic numbers)而非仅仅依赖扩展名来判断文件类型的。准备好了吗?咱们这就开始!

第一部分:引子——扩展名靠谱吗?

咱们先来聊个轻松的话题,你有没有遇到过这样的情况:

  • 你明明下载了一个.jpg文件,结果打开一看,是个视频?
  • 你辛辛苦苦写了个.txt文件,结果别人用Word一打开,乱码一片?

这说明什么?说明文件名扩展名这玩意儿,其实挺不靠谱的! 它就像一个人的外表,可以伪装,可以欺骗。真正决定文件“内在”的,是它的内容。所以,如果仅仅依赖扩展名来判断文件类型,那简直就是盲人摸象,很容易掉坑里。

第二部分:WordPress 文件类型判断的传统方式

WordPress在处理文件上传时,一开始也用过比较简单粗暴的方式,那就是通过扩展名来判断文件类型。 这是wp_check_filetype()函数最基本的功能。 我们可以这样理解,如果文件名的扩展名在WordPress允许的扩展名列表里,那么就认为该文件是允许上传的类型。

function wp_check_filetype( $filename, $mimes = null ) {
    /**
     * Filter the list of mime types.
     *
     * @since 2.0.0
     *
     * @param string[]|null $mimes Array of mime types keyed by their file extension regex.
     */
    $mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ? $mimes : get_allowed_mime_types() ) );

    $type = false;
    $ext  = false;

    foreach ( (array) $mimes as $ext_preg => $mime_match ) {
        $ext_preg = '!.(' . $ext_preg . ')$!i';
        if ( preg_match( $ext_preg, $filename, $matches ) ) {
            $type = $mime_match;
            $ext  = $matches[1];
            break;
        }
    }

    return apply_filters( 'wp_check_filetype', compact( 'ext', 'type', 'proper_filename' ), $filename, $mimes );
}

这段代码的核心在于遍历$mimes数组,这个数组包含了允许上传的扩展名及其对应的MIME类型。使用正则表达式匹配文件名后缀与$mimes中的扩展名,如果匹配成功,则返回对应的MIME类型。

第三部分:文件头的救赎——Magic Numbers

为了解决扩展名不可靠的问题,计算机科学家们想出了一个妙招: 给每种文件类型都定义一个“指纹”,这个“指纹”就存在于文件的开头,被称为“文件头”或者“Magic Number”。

这就好比我们通过DNA来确定一个人的身份,无论他怎么化妆,DNA是变不了的。

举个例子:

  • JPEG文件的开头通常是FF D8 FF E0
  • PNG文件的开头通常是89 50 4E 47
  • GIF文件的开头通常是47 49 46 38

有了这些“指纹”,我们就可以通过读取文件的前几个字节,来判断文件的真实类型,而不用管它的扩展名是什么。

第四部分:wp_check_filetype_and_ext() 函数:更全面的文件类型检查

WordPress为了更准确地判断文件类型,引入了wp_check_filetype_and_ext()函数。这个函数会在wp_check_filetype()的基础上,进一步检查文件的MIME类型和扩展名是否一致,并且会尝试通过读取文件头来判断文件类型。

function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
    $proper_filename = false;

    // 1. 通过扩展名来判断文件类型 (和 wp_check_filetype() 一样)
    $filetype = wp_check_filetype( $filename, $mimes );

    // 2. 获取文件的真实 MIME 类型 (通过文件头)
    $real_mime = false;
    if ( function_exists( 'finfo_open' ) ) {
        $finfo     = finfo_open( FILEINFO_MIME_TYPE );
        $real_mime = finfo_file( $finfo, $file );
        finfo_close( $finfo );
    } elseif ( function_exists( 'mime_content_type' ) && @ini_get( 'mime_magic.magicfile' ) ) {
        $real_mime = mime_content_type( $file );
    }

    // 3. 比较通过扩展名判断的 MIME 类型和通过文件头判断的 MIME 类型
    if ( $real_mime && $real_mime != 'application/octet-stream' ) {
        $mime_types = get_allowed_mime_types();
        $exts       = array_keys( $mime_types );
        foreach ( $exts as $ext ) {
            $mime_match = strtolower( $mime_types[ $ext ] );
            if ( strpos( $mime_match, strtolower( $real_mime ) ) !== false ) {
                $ext = preg_replace( '/[^a-z0-9]/i', '', $ext );
                if ( ! $filetype['ext'] || strtolower( $ext ) === strtolower( $filetype['ext'] ) ) {
                    $filetype['ext']  = $ext;
                    $filetype['type'] = $mime_types[ $ext ];
                    break;
                } else {
                    $proper_filename = $filename;
                }
            }
        }
    }
    // ... (省略部分代码)

    return apply_filters( 'wp_check_filetype_and_ext', compact( 'ext', 'type', 'proper_filename' ), $file, $filename, $mimes );
}

让我们分解一下这段代码:

  1. 通过扩展名判断: 首先,它调用wp_check_filetype()函数,根据文件名扩展名来判断文件类型。
  2. 读取文件头: 然后,它尝试通过finfo_open()mime_content_type()函数来读取文件的真实MIME类型。这两个函数都是PHP提供的,可以根据文件头来判断文件类型。
    • finfo_open() 是一个更强大和推荐的方法,它需要fileinfo扩展的支持。
    • mime_content_type() 是一个较老的方法,依赖于mime_magic.magicfile配置。
  3. 比较和验证: 最后,它比较通过扩展名判断的MIME类型和通过文件头判断的MIME类型。如果两者不一致,或者文件头判断出的MIME类型与允许的MIME类型不匹配,那么就认为文件类型有问题。

第五部分:深入finfo_open():探秘文件头判断的原理

finfo_open()函数是PHP的fileinfo扩展提供的,它允许我们读取文件的元数据,包括MIME类型、字符编码等等。它的核心原理是读取文件的前几个字节,然后与一个预定义的“magic number”数据库进行匹配。

这个“magic number”数据库包含了各种文件类型的“指纹”。当finfo_open()读取到文件头时,它会查找数据库,看看哪个文件类型的“指纹”与文件头匹配。如果找到匹配的,那么就认为该文件是对应的类型。

一个简单的例子:

假设我们有一个名为test.jpg的文件,它的内容如下(用十六进制表示):

FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 01 00 01 00 00 FF DB 00 43 00 ...

当我们使用finfo_open()来判断它的MIME类型时,finfo_open()会读取文件的前几个字节(FF D8 FF E0),然后在“magic number”数据库中查找。它会发现FF D8 FF E0是JPEG文件的“指纹”,因此会返回image/jpeg作为MIME类型。

第六部分:get_allowed_mime_types() 函数:定义允许的文件类型

WordPress通过get_allowed_mime_types()函数来定义允许上传的文件类型。这个函数返回一个数组,其中键是扩展名,值是对应的MIME类型。

function get_allowed_mime_types() {
    $wp_mime_types = array(
        'jpg|jpeg|jpe' => 'image/jpeg',
        'gif'          => 'image/gif',
        'png'          => 'image/png',
        'bmp'          => 'image/bmp',
        'tiff|tif'     => 'image/tiff',
        'ico'          => 'image/x-icon',

        'asf|asx|wax|wmv|wmx' => 'video/asf',
        'avi'               => 'video/avi',
        'divx'              => 'video/divx',
        'mov|qt'            => 'video/quicktime',
        'mpeg|mpg|mpe'      => 'video/mpeg',
        'mp4|m4v'           => 'video/mp4',
        'ogv'               => 'video/ogg',
        'webm'              => 'video/webm',
        'mkv'               => 'video/x-matroska',

        '3gp|3gpp'        => 'video/3gpp',
        '3g2|3gp2'        => 'video/3gpp2',

        'txt|asc|c|cc|h'     => 'text/plain',
        'csv'              => 'text/csv',
        'rtx'              => 'text/richtext',
        'css'              => 'text/css',
        'htm|html'         => 'text/html',

        'mp3|m4a|m4b'       => 'audio/mpeg',
        'mpga'            => 'audio/mpeg',
        'ogg|oga'           => 'audio/ogg',
        'wav'               => 'audio/wav',
        'wma'               => 'audio/x-ms-wma',
        'aac'               => 'audio/aac',
        'mka'               => 'audio/x-matroska',

        'pdf'               => 'application/pdf',
        'psd'               => 'application/photoshop',
        'zip'               => 'application/zip',
        'gz|gzip'           => 'application/x-gzip',
        'js'                => 'application/javascript',
        'swf'               => 'application/x-shockwave-flash',
        'xap'               => 'application/x-silverlight-app',
        'rar'               => 'application/rar',

        'odt'               => 'application/vnd.oasis.opendocument.text',
        'ods'               => 'application/vnd.oasis.opendocument.spreadsheet',
        'odp'               => 'application/vnd.oasis.opendocument.presentation',
        'odg'               => 'application/vnd.oasis.opendocument.graphics',
        'odc'               => 'application/vnd.oasis.opendocument.chart',
        'odb'               => 'application/vnd.oasis.opendocument.database',
        'odf'               => 'application/vnd.oasis.opendocument.formula',

        'doc|docx'        => 'application/msword',
        'xls|xlsx'        => 'application/vnd.ms-excel',
        'ppt|pptx|pps|ppsx' => 'application/vnd.ms-powerpoint',

        'woff' => 'application/font-woff',
        'woff2' => 'application/font-woff2',
        'ttf' => 'application/font-ttf',
        'eot' => 'application/vnd.ms-fontobject',
        'svg' => 'image/svg+xml',
        'svgz' => 'image/svg+xml',

        'webp' => 'image/webp',
        'heic' => 'image/heic',
        'heif' => 'image/heif',
    );

    /**
     * Filters the list of allowed mime types and file extensions.
     *
     * @since 2.0.0
     *
     * @param string[] $wp_mime_types Mime types keyed by the file extension regex corresponding to those types.
     */
    return apply_filters( 'mime_types', $wp_mime_types );
}

你可以使用mime_types过滤器来修改这个列表,添加或删除允许的文件类型。

add_filter( 'mime_types', 'my_custom_mime_types' );

function my_custom_mime_types( $mimes ) {
  $mimes['svg'] = 'image/svg+xml';
  return $mimes;
}

第七部分:案例分析:上传恶意文件绕过

假设攻击者想上传一个包含恶意代码的PHP文件,但是WordPress不允许上传.php文件。攻击者可能会尝试以下方法:

  1. 修改扩展名: 将文件重命名为evil.jpg,试图绕过扩展名检查。
  2. 伪造文件头: 在文件开头添加JPEG的文件头(FF D8 FF E0),然后将恶意代码放在后面。

但是,wp_check_filetype_and_ext()函数可以有效地防止这种攻击。

  • 首先,它会根据扩展名判断文件类型为image/jpeg
  • 然后,它会读取文件头,发现文件头是JPEG的“指纹”。
  • 但是,当服务器尝试解析该文件时,会发现文件中包含恶意代码,从而阻止恶意代码的执行。

第八部分:总结

让我们用一个表格来总结一下wp_check_filetype()wp_check_filetype_and_ext()函数的区别:

特性 wp_check_filetype() wp_check_filetype_and_ext()
主要判断依据 扩展名 扩展名 + 文件头
准确性 较低 较高
安全性 较低 较高
是否读取文件内容

总而言之,wp_check_filetype_and_ext()函数通过结合扩展名和文件头来判断文件类型,提高了文件上传的安全性。虽然它不能完全杜绝恶意文件的上传,但可以有效地防止常见的攻击手段。

第九部分:一些需要注意的点

  • fileinfo扩展: finfo_open()函数依赖于PHP的fileinfo扩展。如果你的服务器没有安装这个扩展,那么wp_check_filetype_and_ext()函数将无法读取文件头,安全性会降低。
  • 性能: 读取文件头会增加服务器的负担,特别是对于大文件。因此,你需要权衡安全性和性能。
  • “Magic Number”数据库: finfo_open()函数使用的“magic number”数据库可能会过时。你需要定期更新这个数据库,以支持新的文件类型。
  • MIME类型混淆: 有些文件类型可能会被混淆,例如,一些文本文件可能会被误判为application/octet-stream

结束语

好了,今天的讲座就到这里。希望通过这次讲解,你对WordPress的wp_check_filetype()函数有了更深入的了解。记住,在处理文件上传时,一定要小心谨慎,多重验证,才能确保网站的安全。感谢大家的聆听! 如果有什么问题,欢迎提问。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注