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

咳咳,各位观众老爷,早上好!今天咱就来聊聊WordPress里那个有点意思的家伙:wp_check_filetype()。这玩意儿可不是光看文件名后缀那么简单,它还能“嗅”出文件的真实身份,靠的是文件头!

准备好了吗?咱们这就开始扒它的底裤…哦不,是源码!

一、什么是文件头?(或者说,文件的“DNA”)

简单来说,文件头(File Header)就是文件开头的一段数据,它像文件的身份证一样,告诉我们这个文件到底是什么类型的。每个类型的文件通常都有自己独特的“签名”,也就是特定的字节序列。

举个例子,一个典型的JPEG图像文件,它的文件头通常以FF D8 FF E0开头。 这就像警察叔叔靠指纹来识别罪犯一样,程序可以通过读取文件开头几个字节,并和已知的各种文件类型的签名进行对比,从而判断文件类型。

二、 wp_check_filetype() 的基本结构

wp_check_filetype() 函数位于WordPress核心的 wp-includes/functions.php 文件中。咱们先来看看它的基本骨架:

function wp_check_filetype( $filename, $mimes = null ) {
    if ( empty( $mimes ) ) {
        $mimes = get_allowed_mime_types();
    }

    $file = wp_check_filetype_and_ext( $filename, null, $mimes );

    if ( $file['ext'] === false ) {
        return apply_filters( 'wp_check_filetype', array( 'ext' => false, 'type' => false, 'proper_filename' => false ), $filename, $mimes );
    }

    return apply_filters( 'wp_check_filetype', $file, $filename, $mimes );
}

这段代码的核心在于调用了 wp_check_filetype_and_ext() 函数。 这个函数才是真正干活的,负责检查文件类型和扩展名。 让我们深入到 wp_check_filetype_and_ext() 里面看看。

三、 wp_check_filetype_and_ext() 函数:文件类型鉴定的主力军

这个函数比较长,咱们分段来分析:

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

    // 1. 获取文件名和扩展名
    $wp_filetype = wp_check_filetype( $filename, $mimes );
    $ext         = $wp_filetype['ext'];
    $type        = $wp_filetype['type'];

    // 2. 安全检查:阻止双重扩展名攻击
    if ( strpos( $filename, '..' ) !== false ) {
        return array(
            'ext'           => false,
            'type'          => false,
            'proper_filename' => false,
        );
    }

    // 3. 如果没有提供文件内容,尝试从文件读取
    if ( ! $file ) {
        $file = @file_get_contents( $filename, false, null, 0, 512 ); // 读取文件的前512字节
    }

    // 4. 如果无法读取文件,直接返回
    if ( ! $file ) {
        return array(
            'ext'           => false,
            'type'          => false,
            'proper_filename' => false,
        );
    }

    // 5. 获取允许的 MIME 类型
    if ( empty( $mimes ) ) {
        $mimes = get_allowed_mime_types();
    }

    // 6. 根据文件头检查文件类型
    $real_mime = false;
    $mime_from_header = wp_get_mime_type_from_header( $file );
    if ( $mime_from_header ) {
        $real_mime = $mime_from_header;
    }

    // 7. 如果根据文件头检测到了MIME类型,并且该MIME类型在允许的MIME类型列表中
    if ( $real_mime && in_array( $real_mime, $mimes ) ) {
        $type = $real_mime;
        $ext_possibilities = array_keys( $mimes, $real_mime );
        $ext = reset( $ext_possibilities ); // 获取与该MIME类型关联的第一个扩展名

        // 如果文件名扩展名与实际扩展名不匹配,则尝试找到一个匹配的扩展名
        if ($wp_filetype['ext'] != $ext) {
            $exploded_filename = explode('.', $filename);
            $original_ext = strtolower(end($exploded_filename));

            // 循环遍历与真实MIME类型关联的扩展名,以查找与原始扩展名匹配的扩展名
            foreach($ext_possibilities as $valid_ext) {
                if ($original_ext == $valid_ext) {
                    $ext = $valid_ext;
                    break;
                }
            }
        }
    }

    // 8. 其他情况处理 (例如,MIME类型未检测到,或者检测到的MIME类型不在允许列表中)
    // ... (这部分代码处理比较复杂的情况,咱们先忽略,后面再详细讲)

    // 9. 返回结果
    return apply_filters(
        'wp_check_filetype_and_ext',
        compact( 'ext', 'type', 'proper_filename' ),
        $filename,
        $file,
        $mimes
    );
}

咱们把这个过程分解一下:

  1. 获取文件名和扩展名: 首先,它会调用 wp_check_filetype() 函数,根据文件名来初步判断文件类型和扩展名。这个函数主要靠的就是 pathinfo()$mimes 数组(允许的MIME类型列表)。

  2. 安全检查: 防止目录遍历攻击,比如文件名里包含 ..

  3. 读取文件内容: 如果还没有提供文件内容($file 为空),它会尝试读取文件的前512个字节。 注意这里使用了 @ 符号来抑制 file_get_contents() 可能产生的错误。

  4. 无法读取文件: 如果读取文件失败,直接返回。

  5. 获取允许的MIME类型: 如果没有指定 $mimes,就调用 get_allowed_mime_types() 获取允许上传的文件类型列表。

  6. 根据文件头检查文件类型: 这就是关键所在!调用 wp_get_mime_type_from_header() 函数,根据文件头的内容来判断文件的真实MIME类型。

  7. 如果根据文件头检测到了MIME类型,并且该MIME类型在允许的MIME类型列表中:

    • 使用从文件头检测到的MIME类型更新 $type 变量。
    • 获取与该MIME类型关联的可能的扩展名列表。
    • 如果文件名扩展名与通过文件头检测到的扩展名不匹配,则尝试找到一个匹配的扩展名。
  8. 其他情况处理: 如果根据文件头没有检测到MIME类型,或者检测到的MIME类型不在允许的列表中,就会进入一系列复杂的判断逻辑,尝试根据文件名、扩展名以及其他因素来确定文件类型。我们后面再来详细分析这部分。

  9. 返回结果: 最后,将检测到的扩展名、MIME类型和处理后的文件名,通过 apply_filters() 函数进行过滤,然后返回。

四、 wp_get_mime_type_from_header() 函数:嗅探文件类型的“鼻子”

这个函数是根据文件头判断文件类型的核心。咱们再来看看它的源码:

function wp_get_mime_type_from_header( $string ) {
    $string = substr( $string, 0, 512 ); // 限制读取的长度

    // 各种文件类型的签名
    $signatures = array(
        'gif'     => 'GIF87a',
        'gif'     => 'GIF89a',
        'jpg'     => "xFFxD8xFF",
        'png'     => "x89PNGx0dx0ax1ax0a",
        'bmp'     => 'BM',
        'tiff'    => 'II*',
        'tiff'    => 'MM*',
        'swf'     => 'FWS',
        'swf'     => 'CWS',
        'zip'     => 'PK',
        'rar'     => 'Rar!',
        'psd'     => '8BPS',
        'xml'     => '<?xml',
    );

    // 遍历签名,进行匹配
    foreach ( $signatures as $type => $signature ) {
        if ( 0 === strpos( $string, $signature ) ) {
            // 找到匹配的签名,返回对应的MIME类型
            switch ( $type ) {
                case 'gif':
                    return 'image/gif';
                case 'jpg':
                    return 'image/jpeg';
                case 'png':
                    return 'image/png';
                case 'bmp':
                    return 'image/bmp';
                case 'tiff':
                    return 'image/tiff';
                case 'swf':
                    return 'application/x-shockwave-flash';
                case 'zip':
                    return 'application/zip';
                case 'rar':
                    return 'application/x-rar-compressed';
                case 'psd':
                    return 'image/vnd.adobe.photoshop';
                case 'xml':
                    return 'text/xml';
            }
        }
    }

    return false; // 没有找到匹配的签名
}

这个函数的工作原理很简单:

  1. 限制读取的长度: 只读取文件的前512个字节。
  2. 定义各种文件类型的签名: $signatures 数组存储了各种文件类型的“指纹”。
  3. 遍历签名,进行匹配: 使用 strpos() 函数来判断文件开头是否包含某个签名。
  4. 找到匹配的签名,返回对应的MIME类型: 如果找到匹配的签名,就返回对应的MIME类型。
  5. 没有找到匹配的签名: 如果遍历完所有签名都没有找到匹配的,就返回 false

五、 其他情况处理:当文件头“失灵”的时候

回到 wp_check_filetype_and_ext() 函数,咱们再来看看那段被我们忽略的“其他情况处理”代码:

    // 8. 其他情况处理 (例如,MIME类型未检测到,或者检测到的MIME类型不在允许列表中)
    if ( ! $real_mime || ! in_array( $real_mime, $mimes ) ) {
        // 如果已经检测到了类型,但是不在允许列表中
        if ( $type && ! in_array( $type, $mimes ) ) {
            return array(
                'ext'           => false,
                'type'          => false,
                'proper_filename' => false,
            );
        }

        // 如果没有检测到类型,尝试根据扩展名来确定类型
        if ( ! $type && $ext ) {
            $mime_from_ext = false;
            foreach ( $mimes as $mime ) {
                $exts = array_keys( $mimes, $mime );
                if ( in_array( $ext, $exts ) ) {
                    $mime_from_ext = $mime;
                    break;
                }
            }

            if ( $mime_from_ext ) {
                $type = $mime_from_ext;
            } else {
                return array(
                    'ext'           => false,
                    'type'          => false,
                    'proper_filename' => false,
                );
            }
        }

        // 如果仍然没有确定类型,返回失败
        if ( ! $type ) {
            return array(
                'ext'           => false,
                'type'          => false,
                'proper_filename' => false,
            );
        }
    }

    // 9. 返回结果
    return apply_filters(
        'wp_check_filetype_and_ext',
        compact( 'ext', 'type', 'proper_filename' ),
        $filename,
        $file,
        $mimes
    );

这段代码主要处理以下几种情况:

  • 检测到了MIME类型,但不在允许列表中: 直接返回失败。这是一种安全措施,防止上传不安全的文件类型。
  • 没有检测到类型,但有扩展名: 尝试根据扩展名来确定类型。它会在 $mimes 数组中查找与该扩展名关联的MIME类型。
  • 仍然没有确定类型: 如果经过以上步骤仍然无法确定文件类型,就返回失败。

六、 举个栗子:实战演练

假设我们要上传一个名为 myimage.jpg 的文件。

  1. wp_check_filetype() 函数首先会根据文件名判断出扩展名是 jpg,并尝试在 $mimes 数组中找到对应的MIME类型。
  2. 然后,wp_check_filetype_and_ext() 函数会读取文件的前512个字节。
  3. wp_get_mime_type_from_header() 函数会检查文件头,如果发现文件头包含 xFFxD8xFF,它就会判断出文件的真实MIME类型是 image/jpeg
  4. 如果 image/jpeg 在允许的MIME类型列表中,那么函数最终会返回 array( 'ext' => 'jpg', 'type' => 'image/jpeg', 'proper_filename' => 'myimage.jpg' )

但是,如果有人恶意地将一个PHP脚本重命名为 myimage.jpg,那么:

  1. wp_check_filetype() 函数仍然会判断出扩展名是 jpg
  2. wp_check_filetype_and_ext() 函数读取文件内容。
  3. wp_get_mime_type_from_header() 函数会检查文件头,由于PHP脚本的文件头通常不是图片格式的签名,所以它会返回 false
  4. wp_check_filetype_and_ext() 函数会发现根据文件头无法确定文件类型,并且文件名的扩展名是jpg, 那么会尝试从允许的 mime types 中查找与jpg相关的类型。
  5. 如果没有找到,或者检测到的是不被允许的MIME类型,函数就会返回 array( 'ext' => false, 'type' => false, 'proper_filename' => false ),从而阻止恶意脚本的上传。

七、 总结:文件类型检测的艺术

wp_check_filetype()wp_check_filetype_and_ext() 函数是WordPress中文件类型检测的重要组成部分。 它们通过以下方式来确保上传文件的安全性和可靠性:

  • 基于扩展名的初步判断: 快速判断文件类型。
  • 基于文件头的深度检测: 防止恶意用户通过修改文件扩展名来上传不安全的文件。
  • 允许的MIME类型列表: 限制允许上传的文件类型,进一步提高安全性。

八、 缺陷与改进

当然,这种方法也不是万无一失的。

  • 文件头可以被伪造: 虽然修改文件头会破坏文件的完整性,但在某些情况下,攻击者仍然可以通过精心构造的文件来绕过检测。
  • 依赖于已知的签名: 如果遇到新的文件类型,wp_get_mime_type_from_header() 函数可能无法识别。

为了提高文件类型检测的准确性和安全性,可以考虑以下改进:

  • 使用更强大的文件类型检测库: 例如,使用 libmagic 库,它可以更准确地识别文件类型。
  • 结合多种检测方法: 除了文件头和扩展名,还可以考虑文件内容的其他特征,例如文件大小、图像尺寸等。
  • 定期更新签名库: 及时添加新的文件类型的签名,以应对新的攻击方式。

九、 表格总结

为了方便大家理解,我们用一个表格来总结一下:

函数名 作用 关键技术
wp_check_filetype() 根据文件名判断文件类型和扩展名 pathinfo(), $mimes 数组
wp_check_filetype_and_ext() 综合文件名、文件头和允许的MIME类型列表,判断文件的真实类型 file_get_contents(), wp_get_mime_type_from_header(), $mimes 数组
wp_get_mime_type_from_header() 根据文件头判断文件的MIME类型 $signatures 数组, strpos()

十、 最后的话

好了,今天的讲座就到这里。希望通过这次对 wp_check_filetype() 函数的源码分析,能够帮助大家更好地理解WordPress的文件类型检测机制,并在实际开发中更加安全地处理文件上传。

记住,安全无小事,防范于未然!下次再见!

发表回复

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