WordPress wp_check_filetype函数如何基于MIME类型检测文件合法性

好的,下面开始关于 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.jpgwp_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 网站的安全性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注