好的,下面开始关于 WordPress wp_check_filetype
函数如何基于 MIME 类型检测文件合法性的技术讲座。
主题:深入剖析 WordPress wp_check_filetype
函数:基于 MIME 类型的安全文件上传检测
大家好,今天我们来深入探讨 WordPress 中一个非常重要的函数:wp_check_filetype
。这个函数在 WordPress 的文件上传机制中扮演着关键角色,主要负责根据文件名和 MIME 类型来判断上传文件的类型和潜在风险。理解它的工作原理对于开发安全的 WordPress 插件和主题至关重要。
1. wp_check_filetype
函数概览
wp_check_filetype
函数位于 wp-includes/functions.php
文件中。它的主要功能是:
- 确定文件类型: 通过文件名后缀和(或)MIME 类型来识别文件的类型。
- 安全检查: 防止恶意文件(如 PHP 脚本)被上传并执行。
- 返回文件信息: 返回一个包含文件类型、文件扩展名和 MIME 类型的数组。
函数原型如下:
/**
* Retrieve file type based on extension and MIME type.
*
* @since 2.0.0
*
* @param string $filename The name of the file.
* @param string|null $mimes Optional. Array of mime types keyed by the file extension regex corresponding to
* those types.
* @return array Values with extension `ext`, mime type `type`, and boolean `proper_filename`.
*/
function wp_check_filetype( $filename, $mimes = null ) {
// Code implementation will be discussed later
}
参数说明:
$filename
(string): 要检查的文件名,包括扩展名。$mimes
(array, optional): 一个可选的 MIME 类型数组,用于覆盖 WordPress 默认的 MIME 类型列表。 数组的键是文件扩展名的正则表达式,值是对应的 MIME 类型。如果为null
,则使用 WordPress 默认的 MIME 类型。
返回值:
函数返回一个数组,包含以下键:
ext
(string): 文件的扩展名(小写)。如果无法确定扩展名,则为空字符串。type
(string): 文件的 MIME 类型。如果无法确定 MIME 类型,则为空字符串。proper_filename
(bool): 一个布尔值,指示文件名是否包含可接受的字符。这个值用于检查文件名中是否存在潜在的恶意字符,例如<?php
。
2. 默认 MIME 类型列表
WordPress 维护着一个默认的 MIME 类型列表,用于在 wp_check_filetype
函数中进行文件类型判断。 这个列表可以通过 upload_mimes
过滤器进行修改。 默认的 MIME 类型列表定义在 wp-includes/functions.php
文件中, 并在函数内部通过 get_allowed_mime_types()
获取。 以下是一些常见的默认 MIME 类型:
扩展名正则表达式 | MIME 类型 | 描述 |
---|---|---|
jpg|jpeg|jpe |
image/jpeg |
JPEG 图片 |
gif |
image/gif |
GIF 图片 |
png |
image/png |
PNG 图片 |
bmp |
image/bmp |
BMP 图片 |
tiff|tif |
image/tiff |
TIFF 图片 |
pdf |
application/pdf |
PDF 文档 |
doc |
application/msword |
Microsoft Word 文档 |
docx |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
Microsoft Word (OpenXML) |
xls |
application/vnd.ms-excel |
Microsoft Excel 文档 |
xlsx |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
Microsoft Excel (OpenXML) |
ppt |
application/vnd.ms-powerpoint |
Microsoft PowerPoint 文档 |
pptx |
application/vnd.openxmlformats-officedocument.presentationml.presentation |
Microsoft PowerPoint (OpenXML) |
zip |
application/zip |
ZIP 压缩文件 |
rar |
application/x-rar-compressed |
RAR 压缩文件 |
mp3 |
audio/mpeg |
MP3 音频文件 |
wav |
audio/wav |
WAV 音频文件 |
mp4|m4v |
video/mp4 |
MP4 视频文件 |
mov |
video/quicktime |
QuickTime 视频文件 |
avi |
video/x-msvideo |
AVI 视频文件 |
3. wp_check_filetype
函数的实现细节
现在让我们深入了解 wp_check_filetype
函数的实现细节。为了便于讲解,我将函数代码分解成几个关键部分并逐一解释。
function wp_check_filetype( $filename, $mimes = null ) {
$proper_filename = true;
$type = '';
$ext = '';
// Files names containing null bytes can be dangerous.
if ( false !== strpos( $filename, "" ) ) {
$filename = str_replace( "", '', $filename );
$proper_filename = false;
}
// Files names containing `<` or `>` can be dangerous.
if ( false !== strpos( wp_basename( $filename ), '<' ) || false !== strpos( wp_basename( $filename ), '>' ) ) {
$proper_filename = false;
}
// Use wp_basename() to prevent directory traversal attacks.
$filename = wp_basename( $filename );
if ( empty( $mimes ) ) {
$mimes = get_allowed_mime_types();
}
$file = wp_check_filetype_and_ext( $filename, $mimes, $type, $ext );
if ( ! empty( $file['ext'] ) && ! empty( $file['type'] ) ) {
$ext = $file['ext'];
$type = $file['type'];
} else {
$ext = false;
$type = false;
}
return compact( 'ext', 'type', 'proper_filename' );
}
3.1. 文件名安全性检查
$proper_filename = true;
// Files names containing null bytes can be dangerous.
if ( false !== strpos( $filename, "" ) ) {
$filename = str_replace( "", '', $filename );
$proper_filename = false;
}
// Files names containing `<` or `>` can be dangerous.
if ( false !== strpos( wp_basename( $filename ), '<' ) || false !== strpos( wp_basename( $filename ), '>' ) ) {
$proper_filename = false;
}
// Use wp_basename() to prevent directory traversal attacks.
$filename = wp_basename( $filename );
这部分代码主要进行以下安全检查:
- Null 字节检查: 检查文件名是否包含 null 字节 (
)。Null 字节可以被用于截断文件名,绕过后续的检查。如果发现 null 字节,则将其移除,并将
$proper_filename
设置为false
。 <
和>
字符检查: 检查文件名是否包含<
或>
字符。这些字符可能被用于 HTML 注入攻击。如果发现这些字符,则将$proper_filename
设置为false
。 使用wp_basename()
函数来提取文件名,防止目录遍历攻击。wp_basename()
函数: 使用wp_basename()
函数从完整的文件路径中提取文件名。这可以防止目录遍历攻击,例如,攻击者可能上传一个名为../../../../wp-config.php
的文件,试图覆盖 WordPress 的配置文件。
3.2. 获取 MIME 类型列表
if ( empty( $mimes ) ) {
$mimes = get_allowed_mime_types();
}
如果调用函数时没有提供 $mimes
参数,则使用 get_allowed_mime_types()
函数获取 WordPress 允许的 MIME 类型列表。 get_allowed_mime_types()
函数会应用 upload_mimes
过滤器,允许插件和主题修改 MIME 类型列表。
3.3. 调用 wp_check_filetype_and_ext
函数
$file = wp_check_filetype_and_ext( $filename, $mimes, $type, $ext );
if ( ! empty( $file['ext'] ) && ! empty( $file['type'] ) ) {
$ext = $file['ext'];
$type = $file['type'];
} else {
$ext = false;
$type = false;
}
wp_check_filetype
函数的核心逻辑实际上委托给了 wp_check_filetype_and_ext
函数。 wp_check_filetype_and_ext
函数会根据文件名后缀和 MIME 类型来确定文件的类型。 如果 wp_check_filetype_and_ext
成功确定了文件的扩展名和 MIME 类型,则将它们赋值给 $ext
和 $type
变量;否则,将 $ext
和 $type
设置为 false
。
4. wp_check_filetype_and_ext
函数解析
wp_check_filetype_and_ext
函数是真正执行文件类型检查的函数。 让我们深入了解它的实现。
/**
* Attempts to determine the file type and extension from the filename and the mime types defined.
*
* @since 5.1.0
*
* @param string $filename Path to the filename of the file.
* @param string $mimes Optional. Key is the file extension with the value as the mime type.
* @param string $type Optional. * @param string $ext Optional.
* @return array Values with extension `ext`, mime type `type`.
*/
function wp_check_filetype_and_ext( $filename, $mimes = null, $type = '', $ext = '' ) {
$type_and_ext = array(
'ext' => false,
'type' => false,
);
// Use wp_basename() to prevent directory traversal attacks.
$filename = wp_basename( $filename );
if ( empty( $mimes ) ) {
$mimes = get_allowed_mime_types();
}
$file_extension = strtolower( substr( strrchr( $filename, '.' ), 1 ) );
if ( preg_match( '/^[a-zA-Z0-9]{1,5}$/', $file_extension ) ) {
if ( isset( $mimes[ $file_extension ] ) ) {
$type_and_ext['ext'] = $file_extension;
$type_and_ext['type'] = $mimes[ $file_extension ];
return $type_and_ext;
}
}
// If we haven't been able to determine a file type from the file extension alone,
// try to use finfo to look it up.
if ( function_exists( 'finfo_open' ) ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mime_type = finfo_file( $finfo, $filename );
finfo_close( $finfo );
if ( $mime_type ) {
$mime_type = sanitize_mime_type( $mime_type );
foreach ( $mimes as $ext_pattern => $mime_match ) {
if ( preg_match( '#' . $ext_pattern . '#i', $filename ) && $mime_type == $mime_match ) {
$type_and_ext['ext'] = $file_extension;
$type_and_ext['type'] = $mime_type;
return $type_and_ext;
}
}
}
}
return $type_and_ext;
}
4.1. 获取文件扩展名
// Use wp_basename() to prevent directory traversal attacks.
$filename = wp_basename( $filename );
if ( empty( $mimes ) ) {
$mimes = get_allowed_mime_types();
}
$file_extension = strtolower( substr( strrchr( $filename, '.' ), 1 ) );
这部分代码首先使用 wp_basename()
函数提取文件名,然后使用 strrchr()
函数查找文件名中最后一个 .
字符,并提取其后面的字符串作为文件扩展名。 strtolower()
函数将扩展名转换为小写,以确保大小写不敏感的匹配。
4.2. 基于扩展名匹配 MIME 类型
if ( preg_match( '/^[a-zA-Z0-9]{1,5}$/', $file_extension ) ) {
if ( isset( $mimes[ $file_extension ] ) ) {
$type_and_ext['ext'] = $file_extension;
$type_and_ext['type'] = $mimes[ $file_extension ];
return $type_and_ext;
}
}
这部分代码首先使用正则表达式 ^[a-zA-Z0-9]{1,5}$
验证文件扩展名是否只包含字母和数字,并且长度在 1 到 5 个字符之间。 如果扩展名有效,则检查 $mimes
数组中是否存在与该扩展名对应的 MIME 类型。如果存在,则将扩展名和 MIME 类型存储在 $type_and_ext
数组中,并返回该数组。
4.3. 使用 finfo
函数获取 MIME 类型
// If we haven't been able to determine a file type from the file extension alone,
// try to use finfo to look it up.
if ( function_exists( 'finfo_open' ) ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mime_type = finfo_file( $finfo, $filename );
finfo_close( $finfo );
if ( $mime_type ) {
$mime_type = sanitize_mime_type( $mime_type );
foreach ( $mimes as $ext_pattern => $mime_match ) {
if ( preg_match( '#' . $ext_pattern . '#i', $filename ) && $mime_type == $mime_match ) {
$type_and_ext['ext'] = $file_extension;
$type_and_ext['type'] = $mime_type;
return $type_and_ext;
}
}
}
}
如果无法通过文件扩展名确定 MIME 类型,则尝试使用 finfo
函数来获取 MIME 类型。finfo
函数是 PHP 的一个扩展,可以根据文件的内容来确定文件的 MIME 类型。
function_exists( 'finfo_open' )
: 检查finfo_open
函数是否存在,确保finfo
扩展已启用。finfo_open( FILEINFO_MIME_TYPE )
: 创建一个finfo
资源,并指定要获取 MIME 类型。finfo_file( $finfo, $filename )
: 使用finfo
资源打开文件,并获取其 MIME 类型。finfo_close( $finfo )
: 关闭finfo
资源。sanitize_mime_type( $mime_type )
: 对获取到的MIME类型进行过滤- 遍历
$mimes
数组,查找与文件名和 MIME 类型都匹配的条目。 如果找到匹配的条目,则将扩展名和 MIME 类型存储在$type_and_ext
数组中,并返回该数组。
5. 使用示例
以下是一些使用 wp_check_filetype
函数的示例:
示例 1:检查文件的类型
$filename = 'my-image.jpg';
$filetype = wp_check_filetype( $filename );
if ( $filetype['ext'] === 'jpg' && $filetype['type'] === 'image/jpeg' ) {
echo '文件类型是 JPEG 图片';
} else {
echo '文件类型未知或不允许';
}
示例 2:使用自定义 MIME 类型列表
$filename = 'my-custom-file.xyz';
$mimes = array(
'xyz' => 'application/custom-type',
);
$filetype = wp_check_filetype( $filename, $mimes );
if ( $filetype['ext'] === 'xyz' && $filetype['type'] === 'application/custom-type' ) {
echo '文件类型是自定义类型';
} else {
echo '文件类型未知或不允许';
}
示例 3:使用 upload_mimes
过滤器添加新的 MIME 类型
function my_custom_mime_types( $mimes ) {
$mimes['xyz'] = 'application/custom-type';
return $mimes;
}
add_filter( 'upload_mimes', 'my_custom_mime_types' );
$filename = 'my-custom-file.xyz';
$filetype = wp_check_filetype( $filename );
if ( $filetype['ext'] === 'xyz' && $filetype['type'] === 'application/custom-type' ) {
echo '文件类型是自定义类型';
} else {
echo '文件类型未知或不允许';
}
6. 安全注意事项
虽然 wp_check_filetype
函数提供了一定的安全保护,但仍然需要注意以下事项:
- 不要完全依赖于客户端提供的 MIME 类型: 客户端提供的 MIME 类型可能被篡改。 应该始终使用服务器端的
finfo
函数来验证 MIME 类型。 - 对上传的文件进行进一步的安全检查:
wp_check_filetype
函数只能确定文件的类型,不能保证文件是安全的。 应该对上传的文件进行进一步的安全检查,例如,扫描病毒、检查恶意代码等。 - 限制上传的文件大小: 应该限制上传的文件大小,防止恶意用户上传大型文件导致服务器资源耗尽。
- 使用安全的目录存储上传的文件: 应该使用安全的目录存储上传的文件,防止未经授权的访问。
7. 绕过和防御
攻击者可能会尝试绕过 wp_check_filetype
函数的检查,上传恶意文件。 以下是一些常见的绕过技术和防御措施:
- 双重扩展名: 攻击者可能使用双重扩展名,例如
evil.php.jpg
。wp_check_filetype
函数可能会错误地将其识别为 JPG 图片。 防御措施:检查文件名中是否包含多个扩展名,并拒绝包含多个扩展名的文件。 - MIME 类型欺骗: 攻击者可能修改文件的 MIME 类型,使其与允许的类型匹配。 防御措施:使用
finfo
函数验证 MIME 类型,并确保服务器正确配置了 MIME 类型。 - Null 字节注入: 攻击者可能在文件名中注入 null 字节,截断文件名,绕过后续的检查。 防御措施:在文件名中查找并移除 null 字节。
- 文件内容欺骗: 攻击者可能在文件内容中嵌入恶意代码,例如 PHP 脚本。 防御措施:扫描上传的文件,查找恶意代码。
8. 如何修改允许上传的文件类型
可以通过 upload_mimes
过滤器来修改允许上传的文件类型。 以下是一个示例:
function my_custom_upload_mimes( $existing_mimes ) {
$existing_mimes['svg'] = 'image/svg+xml';
return $existing_mimes;
}
add_filter( 'upload_mimes', 'my_custom_upload_mimes' );
这段代码允许上传 SVG 文件。 请注意,允许上传更多类型的文件可能会增加安全风险。 应该只允许上传必要的文件类型,并采取额外的安全措施来保护服务器。
9. sanitize_mime_type
函数
该函数位于wp-includes/formatting.php
,用于清理 MIME 类型字符串,移除其中的非法字符,确保其符合预期格式。这有助于防止 MIME 类型欺骗和安全漏洞。
/**
* Sanitize a mime type.
*
* Removes anything that isn't a letter, number, or slash.
*
* @since 3.1.0
*
* @param string $mime_type Mime type to sanitize.
* @return string Sanitized mime type.
*/
function sanitize_mime_type( $mime_type ) {
return preg_replace( '/[^a-zA-Z0-9/]/', '', $mime_type );
}
该函数使用正则表达式 /[^a-zA-Z0-9/]/
移除所有非字母、数字和斜杠的字符。
10. 关于文件上传安全,以下几点需要牢记
wp_check_filetype
函数是 WordPress 文件上传安全机制的重要组成部分。它通过检查文件名和 MIME 类型来确定文件的类型,并防止恶意文件被上传和执行。然而,wp_check_filetype
函数并不是万无一失的,攻击者可能会尝试绕过它的检查。因此,应该采取额外的安全措施来保护服务器,例如,验证 MIME 类型、扫描病毒、限制文件大小、使用安全的目录存储上传的文件等等。 深入理解 wp_check_filetype
函数的工作原理,并采取适当的安全措施,可以有效地提高 WordPress 网站的安全性。