嘿,大家好!今天咱们来扒一扒 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_preg
是jpg|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 | GIF87a 或 GIF89a |
WAV | RIFF 和 WAVE (需要同时存在) |
%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 源码。 拜拜!