暂无简介
[TOC]
简介
- PHP文件上传实现规范为RFC1867
- 实验环境为 PHP 7.3.4 + nginx 1.20.1,关于上传部分的相关源码在github,PHP解析
multipart/form-data
http请求体的入口函数在SAPI_POST_HANDLER_FUNC
- PHP源码调试环境参考
- PHP示例代码
<?php
var_dump($_FILES);
?>
- 文件解析的简要流程如下
TRICKS
前向截断
\
和/
会对文件名进行前向截断,类似info.txt/info.php
的文件名经php处理后会变成info.php
- 调用栈如下
- 其中有一段注释如下,其本意是为了解决IE上传文件时传递全路径名的问题
/* The \ check should technically be needed for win32 systems only where
* it is a valid path separator. However, IE in all it's wisdom always sends
* the full path of the file on the user's filesystem, which means that unless
* the user does basename() they get a bogus file name. Until IE's user base drops
* to nill or problem is fixed this code must remain enabled for all systems. */
- 关键函数在php_ap_basename,该函数会寻找
\
和/
字符最后出现的位置,并从该位置截断字符串,从而造成了前向的截断
static char *php_ap_basename(const zend_encoding *encoding, char *path)
{
char *s = strrchr(path, '\\');
char *s2 = strrchr(path, '/');
if (s && s2) {
if (s > s2) {
++s;
} else {
s = ++s2;
}
return s;
} else if (s) {
return ++s;
} else if (s2) {
return ++s2;
}
return path;
}
后向截断
00
会对文件名进行后向截断,类似info.php(00)xxx
的文件名经php处理过后会变成info.php
- 在解析
header
的时候,仅对内存进行了copy,内存视图如下
- 后续解析
filename
时候使用strlen
获取filename
长度,strlen
原型如下,在遇到\0
,即内存中的00
时,认为字符串结束了,也就造成了截断
头文件:#include <string.h>
strlen()函数用来计算字符串的长度,其原型为:
unsigned int strlen (char *s);
【参数说明】s为指定的字符串。
strlen()用来计算指定的字符串s 的长度,不包括结束字符"\0"。
同样的,
00
可以对$_POST
变量名也可以进行截断,对$GET
,$_COOKIE
等变量名添加00
会导致400
错误- 示例代码
<?php var_dump($_POST);
- 正常请求
- 在
$_POST
变量名中添加00
后,可以看到postxxx
变为了post
- 但是在
$_POST
变量值中添加00
则不受影响,但是会使得字符串长度加1
文件名末尾的\
字符会被忽略
- 如图所示
- 关键函数在php_ap_getword,当出现
\+quote
这样相连的两个字符时,会忽略\
只取quote
的值
static char *php_ap_getword(const zend_encoding *encoding, char **line, char stop)
{
char *pos = *line, quote;
char *res;
while (*pos && *pos != stop) {
if ((quote = *pos) == '"' || quote == '\'') {
++pos;
while (*pos && *pos != quote) {
// 此处会忽略 \ 字符
if (*pos == '\\' && pos[1] && pos[1] == quote) {
pos += 2;
} else {
++pos;
}
}
if (*pos) {
++pos;
}
} else ++pos;
}
if (*pos == '\0') {
res = estrdup(*line);
*line += strlen(*line);
return res;
}
res = estrndup(*line, pos - *line);
while (*pos == stop) {
++pos;
}
*line = pos;
return res;
}
;
可以影响文件名解析的结果
- 类似
filename=info.php;.txt;
这样的字符串经过PHP处理后,会变成info.php
,注意filename
的值没有用双引号包裹,用双引号包裹会导致失败
- 在解析
Content-Disposition
时,会先使用;
符号进行分词,然后使用=
进行分词。所以,类似filename=info.php;.txt;
这样的字符串第一次分词后结果为filename=info.php
和/txt
,第二次分词时就将filename
解析为了info.php
,大致流程如下
SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* \{\