深入解读 WordPress `wp_check_filetype()` 函数源码:基于文件头的类型判断。

嘿,大家好!今天咱们来扒一扒 WordPress 里的一个“老实人”函数:wp_check_filetype()。 别看它名字平平无奇,但它可是 WordPress 内容安全和文件上传的核心守门员之一。 它就像个文件侦探,专门通过文件头(magic numbers)来判断文件的真实身份,防止有人冒名顶替,上传一些不怀好意的文件。

一、wp_check_filetype():简单但重要的角色

wp_check_filetype() 的作用很简单:根据文件名和(可选的)文件内容,返回文件的 MIME 类型和文件扩展名。 它的定义藏在 /wp-includes/functions.php 文件里。

先来个最简单的用法示例:

$file = 'my_image.jpg';
$filetype = wp_check_filetype( $file );

echo "MIME Type: " . $filetype['type'] . "<br>";
echo "Extension: " . $filetype['ext'] . "<br>";

如果 my_image.jpg 确实是个 JPG 图片,你会得到类似这样的结果:

MIME Type: image/jpeg
Extension: jpg

是不是很简单? 但魔鬼藏在细节里,咱们要深入源码,看看这个“老实人”是怎么工作的。

二、源码剖析:一步一步揭秘文件类型判断

wp_check_filetype() 函数的源码有点长,咱们分段讲解,尽量做到通俗易懂:

function wp_check_filetype( $filename, $mimes = null ) {
    /**
     * Filter the list of mime types.
     *
     * @since 2.0.0
     *
     * @param array|string[] $mimes Key is the file extension with value as the mime type.
     */
    $mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ? $mimes : get_allowed_mime_types() ) );

    $type = false;
    $ext  = false;

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

    /**
     * Filter the "upload_file" content mime type based on the file extension.
     *
     * @since 2.0.0
     *
     * @param string      $type     File mime type.
     * @param string      $ext      File extension.
     * @param string      $filename The name of the file.
     * @param string[]    $mimes    Key is the file extension with value as the mime type.
     */
    $mime = apply_filters( 'upload_mimes_types', array( 'ext' => $ext, 'type' => $type ), $filename, $mimes );

    if ( empty( $mime['type'] ) ) {
        $mime = wp_check_filetype_and_ext( $filename, '', $mimes );
    }

    /**
     * Filter the return array of file extension and mime type.
     *
     * @since 2.0.0
     *
     * @param string[]    $mime     Key is the file extension with value as the mime type.
     * @param string      $filename The name of the file.
     * @param string[]    $mimes    Key is the file extension with value as the mime type.
     */
    return apply_filters( 'wp_check_filetype', $mime, $filename, $mimes );
}

Step 1: 获取允许的 MIME 类型

$mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ? $mimes : get_allowed_mime_types() ) );

这行代码首先通过 apply_filters 应用了 upload_mimes 过滤器。 这个过滤器允许开发者自定义允许上传的文件类型。 如果没有自定义的 MIME 类型,它会调用 get_allowed_mime_types() 函数来获取 WordPress 默认允许的 MIME 类型列表。

get_allowed_mime_types() 返回一个关联数组,键是文件扩展名,值是对应的 MIME 类型。 比如:

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', // Can also be audio.
    '3g2|3gpp2'    => 'video/3gpp2', // Can also be audio.
    'txt|asc|c|cc|h' => 'text/plain',
    'csv'          => 'text/csv',
    'rtx'          => 'text/richtext',
    'css'          => 'text/css',
    'htm|html'     => 'text/html',
    'vtt'          => 'text/vtt',
    'dfxp'         => 'application/ttaf+xml',
    'js'           => 'application/javascript',
    'json'         => 'application/json',
    'rss|atom|xml' => 'application/xml',
    'woff'         => 'font/woff',
    'woff2'        => 'font/woff2',
    'ttf'          => 'font/ttf',
    'eot'          => 'application/vnd.ms-fontobject',
    'otf'          => 'font/otf',
    'svg'          => 'image/svg+xml',
    'pdf'          => 'application/pdf',
    'doc|docx'     => 'application/msword',
    'pot|pps|ppt'  => 'application/vnd.ms-powerpoint',
    'wri'          => 'application/x-mswrite',
    'xla|xls|xlt'  => 'application/vnd.ms-excel',
    'bmp'          => 'image/bmp',
    'tif|tiff'     => 'image/tiff',
    'asf|asx|wax|wmv|wmx' => 'video/asf',
    'avi'          => 'video/avi',
    'mov|qt'       => 'video/quicktime',
    'mp3|m4a|m4b'  => 'audio/mpeg',
    'mp4|m4v'      => 'video/mp4',
    'ogg|oga'      => 'audio/ogg',
    'ram|rm'       => 'audio/x-pn-realaudio',
    'wav'          => 'audio/wav',
    'wma'          => 'audio/x-ms-wma',
    'mid|midi'     => 'audio/midi',
    'rtf'          => 'application/rtf',
    'zip'          => 'application/zip',
    'gz|gzip'      => 'application/x-gzip',
    'rar'          => 'application/rar',
    '7z'           => 'application/x-7z-compressed',
    'psd'          => 'image/vnd.adobe.photoshop',
    'exe'          => 'application/x-msdownload',
    'flv'          => 'video/x-flv',
    'ai'           => 'application/postscript',
    'dmg'          => 'application/x-apple-diskimage',
    'torrent'      => 'application/x-bittorrent',
    'jar|war|ear'  => 'application/java-archive'
);

Step 2: 根据文件扩展名匹配 MIME 类型

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

这段代码遍历了允许的 MIME 类型列表,并使用正则表达式来匹配文件名。

  • $ext_preg = '!.(' . trim( $ext_preg, ' .' ) . ')$!i'; 这行代码构建了一个正则表达式,用于匹配文件扩展名。 例如,如果 $ext_pregjpg|jpeg|jpe,那么构建出的正则表达式就是 !.(jpg|jpeg|jpe)$!i$ 表示匹配字符串的结尾, i 表示不区分大小写。
  • preg_match( $ext_preg, $filename, $matches ) 这行代码使用正则表达式来匹配文件名。 如果匹配成功,$matches 数组会包含匹配的结果,$matches[1] 会包含匹配到的文件扩展名。
  • 如果匹配成功,$type 会被设置为对应的 MIME 类型,$ext 会被设置为文件扩展名,然后跳出循环。

Step 3: 应用 upload_mimes_types 过滤器

$mime = apply_filters( 'upload_mimes_types', array( 'ext' => $ext, 'type' => $type ), $filename, $mimes );

这里又用到了一个过滤器 upload_mimes_types。 开发者可以使用这个过滤器来修改根据文件扩展名判断出的 MIME 类型。

Step 4: 如果扩展名无法判断,尝试通过文件内容判断

if ( empty( $mime['type'] ) ) {
    $mime = wp_check_filetype_and_ext( $filename, '', $mimes );
}

如果根据文件扩展名无法判断出 MIME 类型(比如,文件没有扩展名),那么会调用 wp_check_filetype_and_ext() 函数,尝试通过文件内容(magic numbers)来判断文件类型。 这才是 wp_check_filetype() 函数的核心部分。

Step 5: 应用 wp_check_filetype 过滤器并返回结果

return apply_filters( 'wp_check_filetype', $mime, $filename, $mimes );

最后,wp_check_filetype() 函数应用了 wp_check_filetype 过滤器,允许开发者修改最终的结果,然后返回一个包含文件扩展名和 MIME 类型的数组。

三、wp_check_filetype_and_ext():深入文件内容识别

wp_check_filetype_and_ext() 函数是 wp_check_filetype() 的核心,它通过读取文件内容(通常是文件头)来判断文件类型。 让我们看看它的源码:

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

    /**
     * Filter the list of mime types.
     *
     * @since 2.0.0
     *
     * @param array|string[] $mimes Key is the file extension with value as the mime type.
     */
    $mimes = apply_filters( 'upload_mimes', ( is_array( $mimes ) ? $mimes : get_allowed_mime_types() ) );

    $type = false;
    $ext  = false;

    // We're renaming the variable here, so the original `$file` remains intact.
    $contents_file = $file;

    // Use MIME info from files, if possible.
    if ( function_exists( 'mime_content_type' ) && @ini_get( 'mime_magic.magicfile' ) ) {
        $mime_type = mime_content_type( $contents_file );

        // MIME type is found.
        if ( $mime_type ) {
            /*
             * In some cases, `mime_content_type` can return unexpected values.
             * The intent here is to use the extension for the filename provided.
             *
             * `$file_ext` will be empty if `$filename` does not have a valid extension.
             */
            $file_ext = wp_check_filetype( $filename, $mimes );
            if ( ! empty( $file_ext['ext'] ) ) {
                foreach ( $mimes as $ext_preg => $mime_match ) {
                    $ext_preg = '!.(' . trim( $ext_preg, ' .' ) . ')$!i';
                    if ( preg_match( $ext_preg, $filename, $matches ) ) {
                        if ( $mime_type === $mime_match ) {
                            $type = $mime_type;
                            $ext  = $matches[1];
                            break;
                        }
                    }
                }
            } else {
                /*
                 * In the event `$filename` does not have an extension,
                 * use the value from `mime_content_type()` if it is a valid MIME type.
                 */
                foreach ( $mimes as $ext_preg => $mime_match ) {
                    if ( $mime_type === $mime_match ) {
                        $type = $mime_type;
                        $ext  = $ext_preg;
                        break;
                    }
                }
            }
        }
    }

    // If PHP doesn't have `mime_content_type` or if `mime_content_type` doesn't detect the MIME type,
    // Then use file headers to detect the MIME type.
    if ( ! $type && function_exists( 'wp_get_mime_type' ) && ! empty( $contents_file ) ) {
        $file_data = wp_get_mime_type( $contents_file );

        if ( $file_data ) {
            $type = $file_data['type'];
            $ext  = $file_data['ext'];
            $proper_filename = $file_data['filename'];
        }
    }

    /*
     * `wp_get_mime_type()` can return a filename that contains the correct extension.
     * This ensures that the returned filename is used.
     */
    if ( $proper_filename ) {
        $filename = $proper_filename;
    }

    /**
     * Filter the return array of file extension and mime type.
     *
     * @since 2.0.0
     *
     * @param string[]    $mime     Key is the file extension with value as the mime type.
     * @param string      $filename The name of the file.
     * @param string      $file     The path to the file.
     * @param string[]    $mimes    Key is the file extension with value as the mime type.
     */
    return apply_filters( 'wp_check_filetype_and_ext', compact( 'ext', 'type', 'proper_filename' ), $filename, $file, $mimes );
}

Step 1: 尝试使用 mime_content_type() 函数

if ( function_exists( 'mime_content_type' ) && @ini_get( 'mime_magic.magicfile' ) ) {
    $mime_type = mime_content_type( $contents_file );

    // MIME type is found.
    if ( $mime_type ) {
        // ... 逻辑 ...
    }
}

这段代码首先检查 PHP 是否启用了 mime_content_type() 函数,并且 mime_magic.magicfile 配置项是否设置了 magic 文件。 mime_content_type() 函数可以通过读取文件内容来判断 MIME 类型。

如果 mime_content_type() 函数成功判断出 MIME 类型,那么会根据文件名和 MIME 类型进行一些额外的检查,以确保结果的准确性。

Step 2: 使用 wp_get_mime_type() 函数(核心部分)

if ( ! $type && function_exists( 'wp_get_mime_type' ) && ! empty( $contents_file ) ) {
    $file_data = wp_get_mime_type( $contents_file );

    if ( $file_data ) {
        $type = $file_data['type'];
        $ext  = $file_data['ext'];
        $proper_filename = $file_data['filename'];
    }
}

如果 mime_content_type() 函数无法判断出 MIME 类型,那么会调用 wp_get_mime_type() 函数。 这才是通过文件头判断文件类型的核心逻辑所在。

四、wp_get_mime_type():文件头侦探的秘密武器

wp_get_mime_type() 函数定义在 /wp-includes/functions.php 文件里。 它读取文件的开头几个字节,然后与预定义的 magic numbers 进行匹配,从而判断文件的真实类型。

function wp_get_mime_type( $file ) {
    $mime = '';
    $ext = '';
    $filename = '';

    if ( ! function_exists( 'getimagesize' ) ) {
        return false;
    }

    // Read in entire file, and use string matching.
    $fp = fopen( $file, 'rb' );
    if ( $fp ) {
        $file_data = fread( $fp, 512 );
        fclose( $fp );
    } else {
        return false;
    }

    // Remove UTF-8 BOM if present.
    $file_data = preg_replace( '/^xefxbbxbf/', '', $file_data );

    // Try to determine file type by extension.
    if ( false !== strpos( $file, '.' ) ) {
        $parts = explode( '.', $file );
        $ext = strtolower( array_pop( $parts ) );
    }

    // If we can't determine the file type, try to guess it based on the file header.
    if ( empty( $ext ) ) {
        if ( 0 === strpos( $file_data, "xffxd8" ) ) {
            $mime = 'image/jpeg';
            $ext = 'jpg';
        } elseif ( 0 === strpos( $file_data, "x89PNGx0dx0ax1ax0a" ) ) {
            $mime = 'image/png';
            $ext = 'png';
        } elseif ( 0 === strpos( $file_data, 'GIF87a' ) || 0 === strpos( $file_data, 'GIF89a' ) ) {
            $mime = 'image/gif';
            $ext = 'gif';
        } elseif ( 0 === strpos( $file_data, 'RIFF' ) && false !== strpos( $file_data, 'WAVE' ) ) {
            $mime = 'audio/wav';
            $ext = 'wav';
        } elseif ( 0 === strpos( $file_data, '%PDF-' ) ) {
            $mime = 'application/pdf';
            $ext = 'pdf';
        } elseif ( 0 === strpos( $file_data, "x4Dx5A" ) ) { // MZ header
            $mime = 'application/x-msdownload';
            $ext = 'exe';
        } elseif ( 0 === strpos( $file_data, 'PK' . "x03x04" ) ) {
            $mime = 'application/zip';
            $ext = 'zip';
        } elseif ( 0 === strpos( $file_data, "x77x4Fx46x46" ) ) {
            $mime = 'font/woff';
            $ext = 'woff';
        } elseif ( 0 === strpos( $file_data, '<?xml' ) ) {
            $mime = 'application/xml';
            $ext = 'xml';
        } elseif ( function_exists( 'simplexml_load_string' ) && @simplexml_load_string( $file_data ) ) {
            // If it's an XML file, attempt to load it.
            $mime = 'application/xml';
            $ext = 'xml';
        } else {
            // Try to use getimagesize() to determine the file type.
            $image_info = @getimagesize( $file );
            if ( isset( $image_info['mime'] ) ) {
                $mime = $image_info['mime'];
                switch ( $image_info[2] ) {
                    case IMAGETYPE_JPEG:
                        $ext = 'jpg';
                        break;
                    case IMAGETYPE_GIF:
                        $ext = 'gif';
                        break;
                    case IMAGETYPE_PNG:
                        $ext = 'png';
                        break;
                    case IMAGETYPE_BMP:
                        $ext = 'bmp';
                        break;
                    case IMAGETYPE_TIFF_II:
                    case IMAGETYPE_TIFF_MM:
                        $ext = 'tiff';
                        break;
                    default:
                        $ext = '';
                        break;
                }
            }
        }
    }

    return compact( 'ext', 'mime', 'filename' );
}

核心逻辑:Magic Numbers 比对

wp_get_mime_type() 函数首先读取文件的开头 512 字节。 然后,它使用 strpos() 函数来检查文件头是否匹配预定义的 magic numbers。

文件类型 Magic Number
JPEG xffxd8
PNG x89PNGx0dx0ax1ax0a
GIF GIF87aGIF89a
WAV RIFFWAVE (需要同时存在)
PDF %PDF-
EXE x4Dx5A (MZ header)
ZIP PK . x03x04
WOFF x77x4Fx46x46
XML <?xml

如果匹配成功,wp_get_mime_type() 函数会设置 $mime$ext 变量,并返回包含这些信息的数组。

使用 getimagesize() 函数

如果文件头无法识别文件类型,wp_get_mime_type() 函数会尝试使用 getimagesize() 函数。 getimagesize() 函数可以读取图像文件的信息,包括 MIME 类型和图像尺寸。

五、安全提示:魔术字节的局限性

虽然 wp_check_filetype()wp_get_mime_type() 函数提供了一定的文件类型验证机制,但它们并非万无一失。

  • 文件头欺骗: 攻击者可以修改文件头,使其看起来像另一种类型的文件。 例如,攻击者可以将一个 PHP 脚本的文件头修改为 GIF89a,使其看起来像一个 GIF 图片。
  • 扩展名欺骗: 攻击者可以更改文件扩展名,使其看起来像另一种类型的文件。

因此,仅仅依靠文件扩展名和文件头来判断文件类型是不够安全的。 为了提高安全性,还需要采取其他措施,例如:

  • 限制上传目录的执行权限: 禁止在上传目录下执行 PHP 脚本。
  • 使用更严格的文件类型验证机制: 例如,使用第三方库来验证文件类型。
  • 对上传的文件进行安全扫描: 检查文件是否包含恶意代码。

六、实战演练:自定义 MIME 类型

假设我们需要允许上传 .svgz 文件(压缩的 SVG 文件)。 默认情况下,WordPress 不允许上传 .svgz 文件。 我们可以使用 upload_mimes 过滤器来添加对 .svgz 文件的支持。

add_filter( 'upload_mimes', 'my_custom_mime_types' );

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

这段代码会将 .svgz 文件添加到允许上传的 MIME 类型列表中。

七、总结:理解细节,才能构建更安全的 WordPress

wp_check_filetype() 函数是 WordPress 中一个重要的文件类型验证函数。 它通过文件扩展名和文件内容来判断文件的真实类型,防止恶意文件上传。

通过深入了解 wp_check_filetype() 函数的源码,我们可以更好地理解 WordPress 的安全机制,并采取相应的措施来提高 WordPress 网站的安全性。

希望今天的讲座对大家有所帮助! 记住,安全无小事,细节决定成败。 下次有机会,咱们再聊聊其他有趣的 WordPress 源码。 拜拜!

发表回复

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