各位同学们,早上好!我是你们今天的WordPress文件上传“解剖师”——老码农。今天咱们就来扒一扒WordPress的wp_handle_upload()
这个函数,看看它到底是怎么处理文件上传的,又是怎么把那些“坏家伙”(恶意文件)挡在门外的。
开场白:文件上传的“那些事儿”
文件上传,听起来简单,不就是把文件从浏览器“嗖”的一声扔到服务器上嘛。但实际上,这里面的门道可不少。你想啊,如果没有安全措施,随便什么人都能上传文件,那服务器岂不是成了“垃圾场”?更可怕的是,如果有人上传了恶意脚本,那整个网站都可能被黑掉。
所以,WordPress在文件上传这块,可是下了不少功夫的。wp_handle_upload()
就是负责处理文件上传的核心函数之一。
正文:深入wp_handle_upload()
的源码
咱们直接上代码,然后逐行分析,保证让你们看得明明白白。
<?php
/**
* Handles upload of a file.
*
* @since 2.0.0
*
* @param array $file An array of data for a single file.
* @type string 'name' The name of the file.
* @type string 'type' The type of the file.
* @type string 'tmp_name' The temporary location of the file.
* @type int 'error' The error code for the file upload.
* @type int 'size' The size of the file in bytes.
* @param array $overrides Optional. Array of key => value arguments to change upload behavior.
* Default empty array. Possible arguments:
* * 'test_form' - Whether to test the form ($_POST) or not.
* Defaults to true. A false value can be passed to bypass the
* 'action' and 'test' form input fields.
* * 'unique_filename_callback' - A callback that takes the directory, filename,
* and extension as arguments, and returns a unique filename.
* Defaults to 'wp_unique_filename'.
* @return array On success, returns an array containing:
* * 'file' - The path to the uploaded file relative to the uploads directory.
* * 'url' - The URL to the uploaded file.
* * 'type' - The mime type of the file.
* * 'error' - False on success.
*
* On failure, returns an array containing:
* * 'error' - An error message.
*/
function wp_handle_upload( $file, $overrides = array() ) {
// These files may not be included since PHP may not have file uploads enabled.
if ( ! function_exists( 'wp_get_current_user' ) ) {
require_once ABSPATH . WPINC . '/pluggable.php';
}
$defaults = array( 'test_form' => true, 'test_size' => true, 'test_upload' => true, 'unique_filename_callback' => 'wp_unique_filename' );
$overrides = wp_parse_args( $overrides, $defaults );
/**
* Filters the upload directory for the current site.
*
* @since 2.0.0
*
* @param array $upload {
* Array of upload directory data.
*
* @type string 'path' The path to the upload directory.
* @type string 'url' The URL to the upload directory.
* @type string 'subdir' The path to the upload directory relative to the base upload directory.
* @type bool 'basedir' Path to the base upload directory.
* @type string 'baseurl' URL to the base upload directory.
* @type string 'error' False on success, otherwise contains the error message.
* }
*/
$upload = apply_filters( 'wp_upload_dir', wp_upload_dir() );
if ( ! empty( $upload['error'] ) ) {
return array( 'error' => $upload['error'] );
}
$tmp_name = $file['tmp_name'];
if ( ! is_uploaded_file( $tmp_name ) ) {
return array( 'error' => __( 'Specified file failed upload test.' ) );
}
// Check filesize.
if ( $overrides['test_size'] && ! ( ( $file['size'] > 0 ) && ( $file['size'] < wp_max_upload_size() ) ) ) {
return array( 'error' => sprintf( __( 'The uploaded file exceeds the maximum allowed size for your site.' ), size_format( wp_max_upload_size() ) ) );
}
$wp_filetype = wp_check_filetype( $file['name'], null );
extract( apply_filters( 'upload_mimes', array( 'ext' => false, 'type' => false ) ) );
// Check file extension.
if ( ( ! empty( $ext ) && ! empty( $type ) ) && ( ! isset( $mimes[ $ext ] ) || strpos( $mimes[ $ext ], $type ) === false ) ) {
return array( 'error' => __( 'Sorry, this file type is not permitted for security reasons.' ) );
}
// Check for form submission.
if ( $overrides['test_form'] ) {
check_admin_referer( 'media-form' );
}
// A properly uploaded file will pass this test.
// It makes sure that it was uploaded via HTTP POST.
if ( $overrides['test_upload'] && ! @is_uploaded_file( $tmp_name ) ) { // phpcs:ignore WordPress.PHP.NoSilencing.MaybeSilent
return array( 'error' => __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.' ) );
}
// Move the file to the uploads directory.
$filename = wp_unique_filename( $upload['path'], $file['name'], $unique_filename_callback );
// Move the file to the uploads directory.
$new_file = $upload['path'] . "/$filename";
if ( false === @move_uploaded_file( $tmp_name, $new_file ) ) { // phpcs:ignore WordPress.PHP.NoSilencing.MaybeSilent
return array( 'error' => sprintf( __( 'Could not move the file to: %s' ), $upload['path'] ) );
}
// Set correct file permissions.
$stat = stat( dirname( $new_file ) );
$perms = $stat['mode'] & 0007777;
$perms = $perms & 0000777 | 0000110;
if ( ! chmod( $new_file, $perms ) ) {
$message = sprintf( __( 'Failed to set file permissions: %s' ), substr( sprintf( '%o', $perms ), -3 ) );
/**
* Fires after an uploaded file's permissions are set.
*
* @since 3.9.0
*
* @param string $new_file Path to the newly uploaded file.
* @param int $perms Permissions of the new file.
* @param int $error Error code.
*/
do_action( 'wp_handle_upload_permissions', $new_file, $perms, 1 );
error_log( $message );
}
// Compute the URL.
$url = $upload['url'] . "/$filename";
return array( 'file' => $new_file, 'url' => $url, 'type' => $wp_filetype['type'] );
}
第一步:准备工作(函数开头)
if ( ! function_exists( 'wp_get_current_user' ) ) {
require_once ABSPATH . WPINC . '/pluggable.php';
}
$defaults = array( 'test_form' => true, 'test_size' => true, 'test_upload' => true, 'unique_filename_callback' => 'wp_unique_filename' );
$overrides = wp_parse_args( $overrides, $defaults );
-
引入
pluggable.php
: 首先,它会检查wp_get_current_user()
这个函数是否存在。如果不存在,就引入pluggable.php
。pluggable.php
里面包含了许多WordPress的核心函数,像用户验证、Cookie处理等等。 即使没有用户验证,也要引入,为后续操作做准备。 -
设置默认参数:
$defaults
数组定义了一些默认的配置项,例如:test_form
: 是否检查表单提交(防止CSRF攻击)。test_size
: 是否检查文件大小。test_upload
: 是否检查文件是否是通过HTTP POST上传的。unique_filename_callback
: 用于生成唯一文件名的回调函数。
-
合并参数:
wp_parse_args()
函数将用户传入的$overrides
参数和$defaults
参数合并,最终得到一个完整的配置数组。
第二步:获取上传目录(wp_upload_dir()
)
$upload = apply_filters( 'wp_upload_dir', wp_upload_dir() );
if ( ! empty( $upload['error'] ) ) {
return array( 'error' => $upload['error'] );
}
-
获取上传目录信息:
wp_upload_dir()
函数会返回一个数组,包含上传目录的路径、URL等信息。键名 含义 path
上传目录的完整路径(服务器上的绝对路径) url
上传目录的URL subdir
子目录(相对于 basedir
的路径)basedir
基础上传目录的完整路径 baseurl
基础上传目录的URL error
如果有错误,则包含错误信息 -
apply_filters('wp_upload_dir', ...)
: 这是一个过滤器钩子,允许开发者修改上传目录的信息。比如说,你可以通过这个钩子将文件上传到云存储服务,而不是本地服务器。 -
检查错误: 如果
wp_upload_dir()
返回的数组中包含error
键,说明获取上传目录失败,直接返回错误信息。
第三步:初步验证(文件是否上传成功)
$tmp_name = $file['tmp_name'];
if ( ! is_uploaded_file( $tmp_name ) ) {
return array( 'error' => __( 'Specified file failed upload test.' ) );
}
- 获取临时文件名:
$file['tmp_name']
包含了上传文件的临时文件名(服务器上的临时存储路径)。 is_uploaded_file()
: 这个函数用于检查文件是否是通过HTTP POST上传的。 这是一个非常重要的安全检查,可以防止恶意用户伪造上传请求。
第四步:文件大小验证(防止上传过大文件)
if ( $overrides['test_size'] && ! ( ( $file['size'] > 0 ) && ( $file['size'] < wp_max_upload_size() ) ) ) {
return array( 'error' => sprintf( __( 'The uploaded file exceeds the maximum allowed size for your site.' ), size_format( wp_max_upload_size() ) ) );
}
wp_max_upload_size()
: 这个函数返回允许上传的最大文件大小(以字节为单位)。 这个值可以在WordPress后台设置,也可以通过php.ini
文件配置。- 检查文件大小: 如果文件大小超过了允许的最大值,就返回错误信息。
第五步:文件类型验证(wp_check_filetype()
和upload_mimes
过滤器)
$wp_filetype = wp_check_filetype( $file['name'], null );
extract( apply_filters( 'upload_mimes', array( 'ext' => false, 'type' => false ) ) );
// Check file extension.
if ( ( ! empty( $ext ) && ! empty( $type ) ) && ( ! isset( $mimes[ $ext ] ) || strpos( $mimes[ $ext ], $type ) === false ) ) {
return array( 'error' => __( 'Sorry, this file type is not permitted for security reasons.' ) );
}
wp_check_filetype()
: 这个函数用于检查文件的类型。它会根据文件的扩展名和MIME类型来判断文件类型。apply_filters('upload_mimes', ...)
: 这是一个过滤器钩子,允许开发者修改允许上传的文件类型。$mimes
数组定义了允许上传的文件类型和对应的MIME类型。- 检查文件扩展名和MIME类型: 如果文件的扩展名和MIME类型不在允许的列表中,就返回错误信息。
第六步:表单验证(防止CSRF攻击)
if ( $overrides['test_form'] ) {
check_admin_referer( 'media-form' );
}
check_admin_referer()
: 这个函数用于验证表单提交的nonce值。 Nonce是一种安全令牌,用于防止跨站请求伪造(CSRF)攻击。'media-form'
是一个action,WordPress会生成一个对应的nonce值,并在表单中包含这个nonce值。check_admin_referer()
会验证表单提交的nonce值是否与生成的nonce值匹配。
第七步:再次确认文件是否上传成功
if ( $overrides['test_upload'] && ! @is_uploaded_file( $tmp_name ) ) { // phpcs:ignore WordPress.PHP.NoSilencing.MaybeSilent
return array( 'error' => __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.' ) );
}
为了确保万无一失,这里再次使用is_uploaded_file()
来验证文件是否确实是通过HTTP POST上传的。
第八步:生成唯一文件名(wp_unique_filename()
)
$filename = wp_unique_filename( $upload['path'], $file['name'], $overrides['unique_filename_callback'] );
wp_unique_filename()
: 这个函数用于生成一个唯一的文件名。 它会检查上传目录中是否存在同名的文件,如果存在,则在文件名后面添加一个数字,直到找到一个唯一的文件名。$overrides['unique_filename_callback']
: 允许自定义生成唯一文件名的函数。
第九步:移动文件(move_uploaded_file()
)
$new_file = $upload['path'] . "/$filename";
if ( false === @move_uploaded_file( $tmp_name, $new_file ) ) { // phpcs:ignore WordPress.PHP.NoSilencing.MaybeSilent
return array( 'error' => sprintf( __( 'Could not move the file to: %s' ), $upload['path'] ) );
}
move_uploaded_file()
: 这个函数用于将上传的临时文件移动到指定的目录。 这是一个非常重要的步骤,因为它会将文件从临时存储位置移动到永久存储位置。
第十步:设置文件权限(chmod()
)
$stat = stat( dirname( $new_file ) );
$perms = $stat['mode'] & 0007777;
$perms = $perms & 0000777 | 0000110;
if ( ! chmod( $new_file, $perms ) ) {
$message = sprintf( __( 'Failed to set file permissions: %s' ), substr( sprintf( '%o', $perms ), -3 ) );
/**
* Fires after an uploaded file's permissions are set.
*
* @since 3.9.0
*
* @param string $new_file Path to the newly uploaded file.
* @param int $perms Permissions of the new file.
* @param int $error Error code.
*/
do_action( 'wp_handle_upload_permissions', $new_file, $perms, 1 );
error_log( $message );
}
chmod()
: 这个函数用于设置文件的权限。 WordPress会尝试设置一个合理的权限,以确保文件可以被Web服务器访问,但又不会被恶意用户篡改。 通常是 644 (rw-r–r–)。do_action( 'wp_handle_upload_permissions', ...)
: 这是一个动作钩子,允许开发者在文件权限设置之后执行一些自定义的操作。- 处理权限设置失败的情况: 如果权限设置失败,会记录一条错误日志。
第十一步:构建返回结果
$url = $upload['url'] . "/$filename";
return array( 'file' => $new_file, 'url' => $url, 'type' => $wp_filetype['type'] );
- 构建URL: 根据上传目录的URL和文件名,构建文件的完整URL。
-
返回结果数组: 返回一个数组,包含文件的路径、URL和MIME类型。
键名 含义 file
文件的完整路径(服务器上的绝对路径) url
文件的URL type
文件的MIME类型
安全 considerations
- MIME类型验证: 虽然
wp_check_filetype()
会检查文件的MIME类型,但MIME类型是可以伪造的。 所以,仅仅依赖MIME类型验证是不够的。 - 文件内容扫描: 为了更彻底地防止恶意文件上传,可以考虑使用文件内容扫描工具,例如ClamAV,来扫描上传的文件。
- 禁用执行权限: 确保上传目录没有执行权限,这样即使上传了恶意脚本,也无法执行。
- 输入验证和输出编码: 对所有用户输入进行验证,并对所有输出进行编码,以防止跨站脚本攻击(XSS)。
总结
wp_handle_upload()
函数是WordPress文件上传的核心,它负责处理文件上传的各个环节,并进行安全验证。 通过对文件大小、文件类型、表单提交等进行检查,可以有效地防止恶意文件上传。
总的来说,wp_handle_upload()
函数的设计思路是:
- 多重验证: 从文件大小、文件类型、表单提交等多个方面进行验证,确保上传的文件是安全的。
- 使用过滤器和动作钩子: 允许开发者自定义文件上传的行为,例如修改上传目录、修改允许上传的文件类型等。
- 安全第一: 在设计过程中,始终把安全放在第一位,采取多种措施防止恶意文件上传。
好了,今天的讲座就到这里。希望通过今天的讲解,大家对WordPress的文件上传机制有了更深入的了解。 记住,安全无小事,在开发过程中一定要时刻注意安全问题! 下课!