各位听众,早上好!我是今天的主讲人,很高兴能和大家一起探讨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 );
}
让我们分解一下这段代码:
- 通过扩展名判断: 首先,它调用
wp_check_filetype()
函数,根据文件名扩展名来判断文件类型。 - 读取文件头: 然后,它尝试通过
finfo_open()
或mime_content_type()
函数来读取文件的真实MIME类型。这两个函数都是PHP提供的,可以根据文件头来判断文件类型。finfo_open()
是一个更强大和推荐的方法,它需要fileinfo
扩展的支持。mime_content_type()
是一个较老的方法,依赖于mime_magic.magicfile
配置。
- 比较和验证: 最后,它比较通过扩展名判断的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
文件。攻击者可能会尝试以下方法:
- 修改扩展名: 将文件重命名为
evil.jpg
,试图绕过扩展名检查。 - 伪造文件头: 在文件开头添加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()
函数有了更深入的了解。记住,在处理文件上传时,一定要小心谨慎,多重验证,才能确保网站的安全。感谢大家的聆听! 如果有什么问题,欢迎提问。