咳咳,各位观众老爷,早上好!今天咱就来聊聊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
);
}
咱们把这个过程分解一下:
-
获取文件名和扩展名: 首先,它会调用
wp_check_filetype()
函数,根据文件名来初步判断文件类型和扩展名。这个函数主要靠的就是pathinfo()
和$mimes
数组(允许的MIME类型列表)。 -
安全检查: 防止目录遍历攻击,比如文件名里包含
..
。 -
读取文件内容: 如果还没有提供文件内容(
$file
为空),它会尝试读取文件的前512个字节。 注意这里使用了@
符号来抑制file_get_contents()
可能产生的错误。 -
无法读取文件: 如果读取文件失败,直接返回。
-
获取允许的MIME类型: 如果没有指定
$mimes
,就调用get_allowed_mime_types()
获取允许上传的文件类型列表。 -
根据文件头检查文件类型: 这就是关键所在!调用
wp_get_mime_type_from_header()
函数,根据文件头的内容来判断文件的真实MIME类型。 -
如果根据文件头检测到了MIME类型,并且该MIME类型在允许的MIME类型列表中:
- 使用从文件头检测到的MIME类型更新
$type
变量。 - 获取与该MIME类型关联的可能的扩展名列表。
- 如果文件名扩展名与通过文件头检测到的扩展名不匹配,则尝试找到一个匹配的扩展名。
- 使用从文件头检测到的MIME类型更新
-
其他情况处理: 如果根据文件头没有检测到MIME类型,或者检测到的MIME类型不在允许的列表中,就会进入一系列复杂的判断逻辑,尝试根据文件名、扩展名以及其他因素来确定文件类型。我们后面再来详细分析这部分。
-
返回结果: 最后,将检测到的扩展名、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; // 没有找到匹配的签名
}
这个函数的工作原理很简单:
- 限制读取的长度: 只读取文件的前512个字节。
- 定义各种文件类型的签名:
$signatures
数组存储了各种文件类型的“指纹”。 - 遍历签名,进行匹配: 使用
strpos()
函数来判断文件开头是否包含某个签名。 - 找到匹配的签名,返回对应的MIME类型: 如果找到匹配的签名,就返回对应的MIME类型。
- 没有找到匹配的签名: 如果遍历完所有签名都没有找到匹配的,就返回
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
的文件。
wp_check_filetype()
函数首先会根据文件名判断出扩展名是jpg
,并尝试在$mimes
数组中找到对应的MIME类型。- 然后,
wp_check_filetype_and_ext()
函数会读取文件的前512个字节。 wp_get_mime_type_from_header()
函数会检查文件头,如果发现文件头包含xFFxD8xFF
,它就会判断出文件的真实MIME类型是image/jpeg
。- 如果
image/jpeg
在允许的MIME类型列表中,那么函数最终会返回array( 'ext' => 'jpg', 'type' => 'image/jpeg', 'proper_filename' => 'myimage.jpg' )
。
但是,如果有人恶意地将一个PHP脚本重命名为 myimage.jpg
,那么:
wp_check_filetype()
函数仍然会判断出扩展名是jpg
。wp_check_filetype_and_ext()
函数读取文件内容。wp_get_mime_type_from_header()
函数会检查文件头,由于PHP脚本的文件头通常不是图片格式的签名,所以它会返回false
。wp_check_filetype_and_ext()
函数会发现根据文件头无法确定文件类型,并且文件名的扩展名是jpg, 那么会尝试从允许的 mime types 中查找与jpg相关的类型。- 如果没有找到,或者检测到的是不被允许的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的文件类型检测机制,并在实际开发中更加安全地处理文件上传。
记住,安全无小事,防范于未然!下次再见!