深入理解 WordPress `wp_check_filetype()` 函数的源码:如何通过文件头而非扩展名判断文件类型,以增强安全性。

大家好,欢迎来到今天的“文件类型识别大冒险”讲座!我是你们的向导,准备好一起探索WordPress神秘的 wp_check_filetype() 函数,看看它如何像福尔摩斯一样,通过文件头来判断文件类型,而不是简单地看一眼扩展名。

开场白:扩展名的伪装舞会

想象一下,你在参加一个化装舞会,每个人都穿着奇装异服。有人穿着牛仔服,你以为他是牛仔,结果他掏出了一把激光枪,原来他是星际牛仔!扩展名就像这些服装,可以随意改变,但文件头就像人的DNA,很难伪造。

所以,仅仅依靠扩展名来判断文件类型是很危险的。恶意用户可以把一个邪恶的PHP脚本伪装成无害的image.jpg,然后你的网站就可能被攻陷。

正题:wp_check_filetype() 的解剖

wp_check_filetype() 函数是 WordPress 用于检查文件类型的核心函数之一。它藏身在 wp-includes/functions.php 文件中。让我们深入看看它的源码,一步步揭开它的神秘面纱。

/**
 * Retrieve file type based on extension name.
 *
 * @since 2.0.0
 *
 * @param string $file Full path to the file.
 * @param string $mimes Optional. Array of mime types keyed by the file extension regex corresponding to those types.
 *                      If not provided, {@see get_allowed_mime_types()} will be used.
 * @return array An array of information about the file.
 *               array(
 *                 'ext' => string File extension determined from filename.
 *                 'type' => string File mime type.
 *               )
 */
function wp_check_filetype( $file, $mimes = null ) {
    $defaults = array(
        'ext'  => false,
        'type' => false,
    );

    if ( empty( $file ) ) {
        return $defaults;
    }

    $type = false;
    $ext  = false;

    $file_parts = pathinfo( $file );
    if ( isset( $file_parts['extension'] ) ) {
        $ext = strtolower( $file_parts['extension'] );
    }

    if ( empty( $mimes ) ) {
        $mimes = get_allowed_mime_types();
    }

    if ( isset( $mimes[ $ext ] ) ) {
        $type = $mimes[ $ext ];
    }

    return compact( 'ext', 'type' );
}

这个函数首先获取文件扩展名,然后在一个允许的 MIME 类型数组($mimes)中查找对应的 MIME 类型。如果找到了,它就返回扩展名和 MIME 类型。

但是,请注意,这个函数 仅仅根据扩展名来判断文件类型。 这就是我们之前说的,扩展名可以伪装,所以这并不是最安全的方法。

更安全的姿势:wp_check_filetype_and_ext()

WordPress 提供了一个更强大的函数:wp_check_filetype_and_ext()。 让我们看看它的源码:

/**
 * Retrieve file type and optionally filename extension information.
 *
 * Determines file type using a number of checks.
 *
 * @since 3.0.0
 *
 * @param string $file Full path to the file.
 * @param string $mimes Optional. Array of mime types keyed by the file extension regex corresponding to those types.
 *                      If not provided, {@see get_allowed_mime_types()} will be used.
 * @return array An array of information about the file.
 *               array(
 *                 'ext'             => string|false File extension determined from filename.
 *                 'type'            => string|false File mime type.
 *                 'proper_filename' => string|false Sanitized filename, if supplied filename was inadequate.
 *               )
 */
function wp_check_filetype_and_ext( $file, $mimes = null ) {
    $proper_filename = false;

    $real_mime = false;

    // We're going to look through the extension check first,
    // because if wp_check_filetype() says it's bad, then we don't
    // need to run fileinfo.
    $filetype = wp_check_filetype( $file, $mimes );

    $ext = $filetype['ext'];
    $type = $filetype['type'];

    // If wp_check_filetype() says the file is invalid, stop processing.
    if ( ! $type ) {
        return array(
            'ext'             => false,
            'type'            => false,
            'proper_filename' => false,
        );
    }

    // Use MIME type from wp_check_filetype() to look for
    // it in the list of allowed MIME types.
    $allowed_types = get_allowed_mime_types();

    if ( ! in_array( $type, $allowed_types, true ) ) {
        return array(
            'ext'             => false,
            'type'            => false,
            'proper_filename' => false,
        );
    }

    /**
     * Filter the flags used by finfo_open() to detect file types.
     *
     * @since 3.0.0
     *
     * @param int $flags A bitmask of `FILEINFO` constants.
     */
    $finfo_flags = apply_filters( 'wp_check_filetype_and_ext_flags', FILEINFO_MIME_TYPE | FILEINFO_MIME_ENCODING );

    /**
     * Filter the location of the MIME database used by finfo_open().
     *
     * @since 3.0.0
     *
     * @param string|null $mime_db_path Path to the MIME database, or null to use the system default.
     */
    $finfo_db = apply_filters( 'wp_check_filetype_and_ext_mime_db_path', null );

    if ( function_exists( 'finfo_open' ) ) {
        $finfo = finfo_open( $finfo_flags, $finfo_db );

        if ( $finfo ) {
            $real_mime = finfo_file( $finfo, $file );
            finfo_close( $finfo );
        }
    } elseif ( function_exists( 'mime_content_type' ) && @ini_get( 'safe_mode' ) == '' ) {
        $real_mime = mime_content_type( $file );
    }

    // If $real_mime does not match the extension, process the file name.
    if ( $real_mime && $real_mime != $type ) {
        /*
         * Check for file names like 'example.jpeg.png' where the earlier extension
         * is incorrect. We need to remove the incorrect extension.
         */
        $mime_parts = explode( '/', $real_mime );
        $mime_type = $mime_parts[0];
        $possible_extensions = get_allowed_mime_types();

        $found_extension = false;
        foreach ( $possible_extensions as $extension => $possible_type ) {
            if ( strpos( $possible_type, $mime_type . '/' ) === 0 ) {
                $found_extension = $extension;
                break;
            }
        }

        if ( $found_extension ) {
            // Only replace the extension if there's one to replace.
            if ( ! empty( $ext ) ) {
                $proper_filename = str_replace( ".$ext", ".$found_extension", $file );
            } else {
                $proper_filename = $file . ".$found_extension";
            }
            $file = $proper_filename;

            // Reset $ext and $type to match the corrected filename.
            $ext = $found_extension;
            $type = $real_mime;
        } else {
            // The MIME type does not match any allowed extension, so this is an invalid file.
            return array(
                'ext'             => false,
                'type'            => false,
                'proper_filename' => false,
            );
        }
    }

    // Finally, return the findings.
    return array(
        'ext'             => $ext,
        'type'            => $type,
        'proper_filename' => $proper_filename,
    );
}

这个函数做了以下事情:

  1. 先用 wp_check_filetype() 检查扩展名:这算是一个初步的检查,快速过滤掉一些明显不符合规则的文件。
  2. 使用 finfo_open()mime_content_type() 获取真实 MIME 类型finfo_open() 是一个更强大的函数,它通过读取文件头来判断文件类型。如果 finfo_open() 不可用,它会尝试使用 mime_content_type()
  3. 比较扩展名和真实 MIME 类型:如果扩展名和真实 MIME 类型不一致,它会尝试根据真实 MIME 类型找到一个合适的扩展名,并修改文件名。如果找不到合适的扩展名,它会认为文件无效。

文件头的魔法:finfo_open()mime_content_type()

finfo_open() 函数利用了 fileinfo 扩展,它通过读取文件的前几个字节(文件头)来判断文件类型。不同的文件类型有不同的文件头。 例如:

  • JPEG 文件通常以 FF D8 FF 开头。
  • PNG 文件通常以 89 50 4E 47 0D 0A 1A 0A 开头。
  • GIF 文件通常以 47 49 46 38 37 6147 49 46 38 39 61 开头。

mime_content_type() 函数的功能类似,但它可能没有 finfo_open() 准确,而且在某些环境中可能被禁用。

用代码说话:一个简单的例子

假设我们有一个文件 evil.php.jpg,它的内容是一个简单的 PHP 脚本。

<?php
  echo "Hacked!";
?>

如果我们用 wp_check_filetype() 检查它,它会认为这是一个 JPEG 文件,因为它的扩展名是 .jpg

$file = 'evil.php.jpg';
$filetype = wp_check_filetype( $file );
echo "扩展名: " . $filetype['ext'] . "n";
echo "MIME 类型: " . $filetype['type'] . "n";
//输出:
//扩展名: jpg
//MIME 类型: image/jpeg

但是,如果我们用 wp_check_filetype_and_ext() 检查它,它会发现文件头不是 JPEG 的文件头,因此会认为文件无效。

$file = 'evil.php.jpg';
$filetype = wp_check_filetype_and_ext( $file );
echo "扩展名: " . $filetype['ext'] . "n";
echo "MIME 类型: " . $filetype['type'] . "n";
echo "正确文件名: " . $filetype['proper_filename'] . "n";
//输出:
//扩展名:
//MIME 类型:
//正确文件名:

或者,如果开启了PHP的 fileinfo 扩展,它可能会识别出文件是 PHP 脚本,并尝试找到一个合适的扩展名。 但是在默认配置下,WordPress通常不会允许上传PHP脚本,所以这个文件仍然会被拒绝。

get_allowed_mime_types():MIME 类型的白名单

get_allowed_mime_types() 函数返回一个允许上传的 MIME 类型数组。这个数组定义了哪些文件类型是被允许的。

/**
 * Returns the allowed mime types for file uploads.
 *
 * @since 2.0.0
 *
 * @param int|WP_User|null $user Optional. User to check against, defaults to the current user.
 * @return array Array of allowed mime types keyed by their file extension regex.
 */
function get_allowed_mime_types( $user = null ) {
    $t = wp_get_mime_types();

    /**
     * Filters the list of allowed mime types for file uploads.
     *
     * @since 2.0.0
     *
     * @param array $mime_types Mime types keyed by the file extension regex corresponding to those types.
     * @param int|WP_User|null $user       User to check against, defaults to the current user.
     */
    return apply_filters( 'upload_mimes', $t, $user );
}

默认情况下,这个数组包含一些常见的 MIME 类型,如 image/jpegimage/pngapplication/pdf 等。你可以使用 upload_mimes 过滤器来修改这个数组,添加或删除允许的 MIME 类型。

表格总结:函数对比

函数 作用 安全性 依赖
wp_check_filetype() 根据扩展名判断文件类型
wp_check_filetype_and_ext() 根据扩展名和文件头判断文件类型 fileinfo 扩展(推荐)或 mime_content_type() 函数
get_allowed_mime_types() 返回允许上传的 MIME 类型数组 N/A

安全建议:最佳实践

  • 始终使用 wp_check_filetype_and_ext() 函数: 这是更安全的选择,因为它会检查文件头。
  • 严格控制允许上传的 MIME 类型: 只允许上传你需要的 MIME 类型,并定期审查 get_allowed_mime_types() 函数返回的数组。
  • 禁用不必要的 PHP 函数: 如果你不需要 mime_content_type() 函数,可以禁用它,以减少潜在的安全风险。
  • 使用安全的文件存储目录: 将上传的文件存储在一个不能执行 PHP 脚本的目录中。
  • 对上传的文件进行安全扫描: 使用杀毒软件或安全扫描工具对上传的文件进行扫描,以检测潜在的恶意代码。
  • 不要相信用户输入: 永远不要相信用户上传的文件名或任何其他用户提供的数据。
  • 定期更新 WordPress 和插件: 保持 WordPress 和插件的最新版本,以修复已知的安全漏洞。

高级话题:自定义文件类型检测

如果你需要支持 WordPress 默认不支持的文件类型,你可以使用 upload_mimes 过滤器来添加新的 MIME 类型。你还可以编写自己的文件类型检测函数,并将其集成到 WordPress 中。

例如,假设你要支持上传 .svg 文件:

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

这段代码会将 image/svg+xml MIME 类型添加到允许上传的 MIME 类型列表中,并将其与 .svg 扩展名关联起来。

真实案例:WordPress 插件漏洞

很多 WordPress 插件都存在文件上传漏洞,因为它们没有正确地验证文件类型。攻击者可以利用这些漏洞上传恶意文件,从而控制整个网站。

一个典型的案例是,插件使用 wp_check_filetype() 函数来验证文件类型,但没有检查文件头。攻击者可以上传一个伪装成图像的 PHP 脚本,然后通过直接访问该脚本来执行恶意代码。

结束语:安全之路,永无止境

文件类型识别是网站安全的重要组成部分。通过深入理解 wp_check_filetype()wp_check_filetype_and_ext() 函数的源码,我们可以更好地保护我们的网站免受恶意攻击。

记住,安全之路,永无止境。我们需要不断学习新的安全技术,并保持警惕,才能确保我们的网站安全。

感谢大家的参与!希望这次“文件类型识别大冒险”能帮助大家更好地理解 WordPress 的文件类型检测机制,并提高网站的安全性。 下次再见!

发表回复

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