早啊,各位!今天咱们聊聊 WordPress 里的一个幕后英雄:wp_check_filetype()
。 别看它名字平平无奇,但它可是确保你上传的文件没耍花招的关键先生。
一、 为什么单靠扩展名不靠谱?
在深入源码之前,先来聊聊为什么 WordPress 不仅仅依靠文件扩展名来判断文件类型。 想象一下,你把一个 .exe
文件改成 .jpg
扩展名,如果 WordPress 仅仅看扩展名,那岂不是直接让你上传并运行恶意代码了? 这简直是给黑客开了方便之门!
所以,扩展名这种东西太容易伪造了,简直就是个“伪君子”。我们需要更靠谱的依据,那就是文件内容本身。
二、 文件头(Magic Numbers):真正的身份证明
每个文件类型,在文件的开头部分,都有一些特定的字节序列,这就是所谓的“Magic Numbers”或者“文件头”。 它们就像文件的指纹,是文件类型的真正身份证明。
举个例子:
- JPEG 文件通常以
FF D8 FF
开头 - PNG 文件通常以
89 50 4E 47 0D 0A 1A 0A
开头 - GIF 文件通常以
GIF87a
或GIF89a
开头
wp_check_filetype()
的核心思想就是:检查文件的开头部分,看看它是否符合已知文件类型的 Magic Numbers。
三、 wp_check_filetype()
源码剖析
接下来,咱们深入到 wp-includes/functions.php
文件中,看看 wp_check_filetype()
的源码:
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()
自己并没有做什么特别复杂的事情,它实际上是调用了 wp_check_filetype_and_ext()
函数。
所以,关键在于 wp_check_filetype_and_ext()
函数。继续深入:
function wp_check_filetype_and_ext( $filename, $file = null, $mimes = null ) {
$proper_filename = false;
if ( empty( $mimes ) ) {
$mimes = get_allowed_mime_types();
}
$type = false;
$ext = false;
// Get file extension from filename.
$original_ext = pathinfo( $filename, PATHINFO_EXTENSION );
if ( $original_ext ) {
$original_ext = strtolower( $original_ext );
}
// Check for file extension overrides.
$ext = apply_filters( 'wp_check_filetype_and_ext', $ext, $filename, $file, $mimes, $original_ext );
// If extension overrides are provided, short-circuit the rest of the tests.
if ( ! empty( $ext ) ) {
$type = $mimes[ $ext ];
return compact( 'ext', 'type', 'proper_filename' );
}
if ( ! empty( $file ) ) {
$imgsize = @getimagesize( $file );
if ( ! empty( $imgsize['mime'] ) ) {
$type = $imgsize['mime'];
}
}
if ( false === $type && function_exists( 'finfo_open' ) ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
if ( $finfo ) {
$type = finfo_file( $finfo, $file );
finfo_close( $finfo );
}
}
// Check MIME type against allowed types.
$allowed = false;
foreach ( $mimes as $ext_pattern => $mime_type ) {
$pattern = '/.' . preg_quote( $ext_pattern, '/' ) . '$/i';
if ( preg_match( $pattern, $filename ) && $mime_type === $type ) {
$allowed = true;
break;
}
}
if ( ! $allowed ) {
$type = false;
$ext = false;
}
// Look for proper extension based on MIME type.
if ( $type ) {
foreach ( $mimes as $ext_pattern => $mime_type ) {
if ( $mime_type === $type ) {
$ext = $ext_pattern;
break;
}
}
}
return compact( 'ext', 'type', 'proper_filename' );
}
咱们把这个函数拆解成几个关键步骤:
-
获取允许的 MIME 类型:
if ( empty( $mimes ) ) { $mimes = get_allowed_mime_types(); }
首先,它会获取 WordPress 允许上传的所有 MIME 类型。 这些类型存储在一个关联数组
$mimes
中,其中键是文件扩展名,值是 MIME 类型。 例如:array( 'jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'pdf' => 'application/pdf', // ... 更多类型 );
get_allowed_mime_types()
函数定义了这些允许的 MIME 类型。 你可以通过upload_mimes
过滤器来修改允许的 MIME 类型列表。 -
提取原始扩展名:
$original_ext = pathinfo( $filename, PATHINFO_EXTENSION ); if ( $original_ext ) { $original_ext = strtolower( $original_ext ); }
从文件名中提取扩展名,并转换为小写。 记住,这只是一个参考,不能完全信任。
-
检查扩展名覆盖:
$ext = apply_filters( 'wp_check_filetype_and_ext', $ext, $filename, $file, $mimes, $original_ext ); // If extension overrides are provided, short-circuit the rest of the tests. if ( ! empty( $ext ) ) { $type = $mimes[ $ext ]; return compact( 'ext', 'type', 'proper_filename' ); }
这里使用了一个过滤器
wp_check_filetype_and_ext
,允许开发者通过插件或主题来覆盖文件类型检查逻辑。 如果过滤器返回了扩展名,则直接使用该扩展名,并跳过后续的检查。 -
使用
getimagesize()
获取图像类型:if ( ! empty( $file ) ) { $imgsize = @getimagesize( $file ); if ( ! empty( $imgsize['mime'] ) ) { $type = $imgsize['mime']; } }
如果上传的是图像文件,并且提供了文件路径,那么
getimagesize()
函数可以读取图像的文件头信息,并返回图像的 MIME 类型。 这是一个快速且常用的方法来判断图像类型。 注意@
符号用于抑制可能出现的错误。 -
使用
finfo_open()
获取文件类型:if ( false === $type && function_exists( 'finfo_open' ) ) { $finfo = finfo_open( FILEINFO_MIME_TYPE ); if ( $finfo ) { $type = finfo_file( $finfo, $file ); finfo_close( $finfo ); } }
如果
getimagesize()
无法确定文件类型,并且服务器支持finfo
扩展,那么finfo_open()
函数会尝试读取文件的 Magic Numbers,并返回文件的 MIME 类型。finfo
扩展提供了更强大的文件类型检测能力,可以识别更多类型的文件。 -
验证 MIME 类型是否允许:
$allowed = false; foreach ( $mimes as $ext_pattern => $mime_type ) { $pattern = '/.' . preg_quote( $ext_pattern, '/' ) . '$/i'; if ( preg_match( $pattern, $filename ) && $mime_type === $type ) { $allowed = true; break; } } if ( ! $allowed ) { $type = false; $ext = false; }
将通过
getimagesize()
或finfo_open()
获取的 MIME 类型与允许的 MIME 类型列表进行比较。 如果找到匹配的 MIME 类型,并且文件名也符合扩展名模式,那么就认为该文件类型是允许的。 -
查找正确的扩展名:
if ( $type ) { foreach ( $mimes as $ext_pattern => $mime_type ) { if ( $mime_type === $type ) { $ext = $ext_pattern; break; } } }
根据确定的 MIME 类型,查找对应的扩展名。
四、 get_allowed_mime_types()
做了什么?
现在我们来看看get_allowed_mime_types()
这个函数,它定义了WordPress默认允许上传的文件类型:
function get_allowed_mime_types( $user = null ) {
$t = 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|3gp2' => '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',
'mp3|m4a|m4b' => 'audio/mpeg',
'aac' => 'audio/aac',
'ra|ram' => 'audio/x-realaudio',
'wav' => 'audio/wav',
'ogg|oga' => 'audio/ogg',
'flac' => 'audio/flac',
'mid|midi' => 'audio/midi',
'wma' => 'audio/x-ms-wma',
'wax' => 'audio/x-ms-wax',
'mka' => 'audio/x-matroska',
'rtf' => 'application/rtf',
'js' => 'application/javascript',
'pdf' => 'application/pdf',
'odt' => 'application/vnd.oasis.opendocument.text',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'key|keynote' => 'application/vnd.apple.keynote',
'pps|ppt' => 'application/vnd.ms-powerpoint',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'odg' => 'application/vnd.oasis.opendocument.graphics',
'wri' => 'application/x-mswrite',
'eml|msg' => 'message/rfc822',
'exe|com|bin' => 'application/x-msdownload',
'zip|gz|tar|rar|7z' => 'application/zip',
'psd' => 'image/vnd.adobe.photoshop',
'ai' => 'application/postscript',
'mpkg' => 'application/vnd.apple.installer+xml',
'epub' => 'application/epub+zip',
'woff|woff2' => 'font/woff',
'eot' => 'application/vnd.ms-fontobject',
'svg' => 'image/svg+xml',
'jar|war' => 'application/java-archive',
'ttf' => 'font/ttf',
);
/**
* Filters the list of allowed mime types and file extensions.
*
* @since 2.0.0
*
* @param string[] $mime_types Key is the file extension with the leading full stop,
* value is the mime type.
* @param WP_User|null $user The user object, if the user is logged in. `null` if is not.
*/
return apply_filters( 'upload_mimes', $t, $user );
}
这个函数返回一个数组,包含了 WordPress 默认允许上传的文件类型。 数组的键是文件扩展名(支持多个扩展名用 |
分隔),值是对应的 MIME 类型。
重点:你可以使用 upload_mimes
过滤器来添加或删除允许上传的文件类型。 这为你的网站提供了极大的灵活性。
五、 绕过文件类型检查?没那么容易!
尽管 wp_check_filetype()
已经做了很多工作,但仍然有一些绕过文件类型检查的方法。 例如:
- MIME 类型混淆: 某些文件类型可能具有相同的 MIME 类型。 例如,
.3gp
文件可以是视频或音频文件。 - 双重扩展名: 使用类似
filename.jpg.php
的文件名,服务器可能会将其解析为 PHP 文件。 - 文件头注入: 在文件的开头添加合法的图像文件头,然后在后面添加恶意代码。
然而,WordPress 也在不断加强安全措施来防止这些攻击。 例如,WordPress 会:
- 使用
wp_get_image_editor()
来验证图像文件的完整性。 - 禁止直接执行上传目录中的 PHP 文件。
- 定期更新 WordPress 核心和插件,以修复安全漏洞。
六、 总结
wp_check_filetype()
函数是 WordPress 文件上传安全的重要组成部分。 它通过检查文件的 Magic Numbers 和 MIME 类型,来判断文件的真实类型,防止恶意文件上传。
步骤 | 说明 | 使用函数/方法 |
---|---|---|
1. 获取允许的 MIME 类型 | 获取 WordPress 允许上传的所有 MIME 类型列表。 | get_allowed_mime_types() |
2. 提取原始扩展名 | 从文件名中提取扩展名。 | pathinfo() |
3. 检查扩展名覆盖 | 允许开发者通过过滤器覆盖文件类型检查逻辑。 | apply_filters( 'wp_check_filetype_and_ext' ) |
4. 使用 getimagesize() |
如果是图像文件,尝试使用 getimagesize() 读取文件头信息并获取 MIME 类型。 |
getimagesize() |
5. 使用 finfo_open() |
如果 getimagesize() 失败,尝试使用 finfo_open() 读取文件的 Magic Numbers 并获取 MIME 类型。 |
finfo_open() , finfo_file() , finfo_close() |
6. 验证 MIME 类型 | 将获取的 MIME 类型与允许的 MIME 类型列表进行比较,验证文件类型是否允许上传。 | preg_match() |
7. 查找正确的扩展名 | 根据确定的 MIME 类型,查找对应的扩展名。 |
掌握了 wp_check_filetype()
的工作原理,能让你更好地理解 WordPress 的安全机制,并为你的网站提供更可靠的保护。 当然,安全是一个持续的过程,需要不断学习和实践。 希望今天的讲座对你有所帮助! 咱们下次再见!