Nginx-配置误区
2016-03-23 22:21
671 查看
Nginx
这两天网上开始疯传一个“nginx文件类型错误解析漏洞”,这个“漏洞”是这样的:假设有如下的 URL:http://phpvim.net/foo.jpg,当访问 http://phpvim.net/foo.jpg/a.php 时,foo.jpg 将会被执行,如果 foo.jpg 是一个普通文件,那么 foo.jpg 的内容会被直接显示出来,但是如果把一段 php 代码保存为 foo.jpg,那么问题就来了,这段代码就会被直接执行。这对一个 Web 应用来说,所造成的后果无疑是毁灭性的。
关于这个问题,已有高手 laruence 做过详细的分析,这里再多啰嗦几句。
首先不管你是否有用到正则来解析 PATH_INFO,这个漏洞都是存在的。比如下面这个最基本的 nginx 配置:
1 2 3 4 5 6 | <span style="font-size:14px;">location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }</span> |
比如访问下面这个 URL:
1 | [text] view plain copy <span style="font-size: 14px;">http://phpvim.net/foo.jpg/a.php/b.php/c.php</span> |
1 | [text] view plain copy <span style="font-size: 14px;">/home/verdana/public_html/unsafe/foo.jpg/a.php/b.php/c.php</span> |
当 php.ini 中 cgi.fix_pathinfo = 1 时,PHP CGI 以 / 为分隔符号从后向前依次检查如下路径:
12 | [text] view plain copy <span style="font-size: 14px;">/home/verdana/public_html/unsafe/foo.jpg/a.php/b.php/c.php /home/verdana/public_html/unsafe/foo.jpg/a.php/b.php /home/verdana/public_html/unsafe/foo.jpg/a.php /home/verdana/public_html/unsafe/foo.jpg</span> |
PHP 会把这个文件当成 cgi 脚本执行,并赋值路径给 CGI 环境变量——SCRIPT_FILENAME,也就是 $_SERVER['SCRIPT_FILENAME'] 的值了。
在很多使用 php-fpm (<0.6) 的主机中也会出现这个问题,但新的 php-fpm 的已经关闭了 cgi.fix_pathinfo,如果你查看 phpinfo() 页面会发现这个选项已经不存在了,代码 ini_get(“cgi.fix_pathinfo”) 的返回值也是 “false”。
原因是似乎因为 APC 的一个 bug,当 cgi.fix_pathinfo 开启时,PATH_TRANSLATED 有可能是 NULL,从而引起内存异常,造成 php-fpm crash,所以 php-fpm 关闭这个选项。
比如, 有http://www.laruence.com/fake.jpg, 那么通过构造如下的URL, 就可以看到fake.jpg的二进制内容:
<ol><li> </li><li>http://www.laruence.com/fake.jpg/foo.php</li></ol>
为什么会这样呢?
比如, 如下的nginx conf:
<ol><li>location ~ \.php($|/) {</li><li> fastcgi_pass 127.0.0.1:9000;</li><li> fastcgi_index index.php;</li><li> </li><li> set $script $uri;</li><li> set $path_info "";</li><li> if ($uri ~ "^(.+\.php)(/.*)") {</li><li> set $script $1;</li><li> set $path_info $2;</li><li> }</li><li> </li><li> include fastcgi_params;</li><li> fastcgi_param SCRIPT_FILENAME $document_root$script;</li><li> fastcgi_param SCRIPT_NAME $script;</li><li> fastcgi_param PATH_INFO $path_info;</li><li>}</li></ol>
通过正则匹配以后, SCRIPT_NAME会被设置为”fake.jpg/foo.php”, 继而构造成SCRIPT_FILENAME传递个PHP CGI, 但是PHP又为什么会接受这样的参数, 并且把a.jpg解析呢?
这就要说到PHP的cgi SAPI中的参数, fix_pathinfo了:
<ol><li>; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's</li><li>; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok</li><li>; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting</li><li>; this to 1 will cause PHP CGI to fix it's paths to conform to the spec. A setting</li><li>; of zero causes PHP to behave as before. Default is 1. You should fix your scripts</li><li>; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.</li><li>cgi.fix_pathinfo=1</li></ol>
如果开启了这个选项, 那么就会触发在PHP中的如下逻辑:
<ol><li><span class="sh_comment">/*</span></li><li><span class="sh_comment"> * if the file doesn't exist, try to extract PATH_INFO out</span></li><li><span class="sh_comment"> * of it by stat'ing back through the '/'</span></li><li><span class="sh_comment"> * this fixes url's like /info.php/test</span></li><li><span class="sh_comment"> */</span></li><li><span class="sh_keyword">if</span> <span class="sh_symbol">(</span>script_path_translated <span class="sh_symbol">&&</span></li><li> <span class="sh_symbol">(</span>script_path_translated_len <span class="sh_symbol">=</span> <span class="sh_function">strlen</span><span class="sh_symbol">(</span>script_path_translated<span class="sh_symbol">))</span> <span class="sh_symbol">></span> <span class="sh_number">0</span> <span class="sh_symbol">&&</span></li><li> <span class="sh_symbol">(</span>script_path_translated<span class="sh_symbol">[</span>script_path_translated_len<span class="sh_number">-1</span><span class="sh_symbol">]</span> <span class="sh_symbol">==</span> <span class="sh_string">'/'</span> <span class="sh_symbol">||</span></li><li><span class="sh_symbol">....</span><span class="sh_comment">//以下省略.</span></li></ol>
到这里, PHP会认为SCRIPT_FILENAME是fake.jpg, 而foo.php是PATH_INFO, 然后PHP就把fake.jpg当作一个PHP文件来解释执行… So…
这个隐患的危害用小顿的话来说, 是巨大的.
对很多人而言,配置Nginx+PHP无外乎就是搜索一篇教程,然后拷贝粘贴。听上去似乎也没什么问题,可惜实际上网络上很多资料本身年久失修,漏洞百出,如果大家不求甚解,一味的拷贝粘贴,早晚有一天会为此付出代价。
假设我们用PHP实现了一个前端控制器,或者直白点说就是统一入口:把PHP请求都发送到同一个文件上,然后在此文件里通过解析「REQUEST_URI」实现路由。
此时很多教程会教大家这样配置Nginx+PHP:
…
我们有必要先了解一下Nginx配置文件里指令的继承关系:Nginx配置文件分为好多块,常见的从外到内依次是「http」、「server」、「location」等等,缺省的继承关系是从外到内,也就是说内层块会自动获取外层块的值作为缺省值(有例外,详见参考)。
参考:UNDERSTANDING THE NGINX CONFIGURATION
INHERITANCE MODEL
…
让我们先从「index」指令入手吧,在问题配置中它是在「location」中定义的:
参考:Nginx Pitfalls
…
接下来看看「if」指令,说它是大家误解最深的Nginx指令毫不为过:
参考:IfIsEvil and How
nginx “location if” works
…
下面看看「fastcgi_params」配置文件:
原本Nginx只有「fastcgi_params」,后来发现很多人在定义「SCRIPT_FILENAME」时使用了硬编码的方式比如自己修改了fastcgi_param SCRIPT_FILENAME /home/data/magento/$fastcgi_script_name;,于是为了规范用法便引入了「fastcgi.conf」。
不过这样的话就产生一个疑问:为什么一定要引入一个新的配置文件,而不是修改旧的配置文件?这是因为「fastcgi_param」指令是数组型的,和普通指令相同的是:内层替换外层;和普通指令不同的是:当在同级多次使用的时候,是新增而不是替换。换句话说,如果在同级定义两次「SCRIPT_FILENAME」,那么它们都会被发送到后端,这可能会导致一些潜在的问题,为了避免此类情况,便引入了一个新的配置文件。
参考:FASTCGI_PARAMS VERSUS FASTCGI.CONF
– NGINX CONFIG HISTORY
…
此外,我们还需要考虑一个安全问题:在PHP开启「cgi.fix_pathinfo」的情况下,PHP可能会把错误的文件类型当作PHP文件来解析。如果Nginx和PHP安装在同一台服务器上的话,那么最简单的解决方法是用「try_files」指令做一次过滤:
…
依照前面的分析,给出一份改良后的版本,是不是比开始的版本清爽了很多:
补充:因为「location」已经做了限定,所以「fastcgi_index」其实也没有必要。
要想让nginx支持PATH_INFO,首先需要知道什么是pathinfo,为什么要用pathinfo?
pathinfo不是nginx的功能,pathinfo是php的功能。
php中有两个pathinfo,一个是环境变量$_SERVER['PATH_INFO'];另一个是pathinfo函数,pathinfo() 函数以数组的形式返回文件路径的信息;。
nginx能做的只是对$_SERVER['PATH_INFO]值的设置。
下面我们举例说明比较直观。先说php中两种pathinfo的作用,再说如何让nginx支持pathinfo。
php中的两个pathinfo
php中的pathinfo()
pathinfo()函数可以对输入的路径进行判断,以数组的形式返回文件路径的信息,数组包含以下元素。
[dirname] 路径的目录
[basename] 带后缀 文件名
[extension] 文件后缀
[filename] 不带后缀文件名(需php5.2以上版本)
例如
[php]
<?php
print_r(pathinfo("/nginx/test.txt"));
?>
[/php]
输出
Array ( [dirname] => /nginx [basename] => test.txt [extension] => txt [filename] => test )
php中的$_SERVER['PATH_INFO']
PHP中的全局变量$_SERVER['PATH_INFO'],PATH_INFO是一个CGI 1.1的标准,经常用来做为传参载体。
被很多系统用来优化url路径格式,最著名的如THINKPHP框架。
对于下面这个网址:
http://www.test.cn/index.php/test/my.html?c=index&m=search
我们可以得到 $_SERVER['PATH_INFO'] = ‘/test/my.html’,而此时 $_SERVER['QUERY_STRING'] = 'c=index&m=search';
如果不借助高级方法,php中http://www.test.com/index.php?type=search 这样的URL很常见,大多数人可能会觉得不太美观而且对于搜索引擎也是非常不友好的(实际上有没有影响未知),因为现在的搜索引擎已经很智能了,可以收入带参数的后缀网页,不过大家出于整洁的考虑还是想希望能够重写URL,
下面是一段解析利用PATH_INFO的进行重写的非常简单的代码:
[php]
<?php
if(!isset($_SERVER['PATH_INFO']))
{
$pathinfo = 'default';
}
else{
$pathinfo = explode('/', $_SERVER['PATH_INFO']);
}
if(is_array($pathinfo) && !empty($pathinfo))
{
$page = $pathinfo[1];
}
else
{
$page = 'default.php';
}
?>
[/php]
有了以上认识我们就可以介入nginx对$_SERVER['PATH_INFO']支持的问题了。在这之前还要介绍一个php.ini中的配置参数cgi.fix_pathinfo,它是用来对设置cgi模式下为php是否提供绝对路径信息或PATH_INFO信息。没有这个参数之前PHP设置绝对路径PATH_TRANSLATED的值为SCRIPT_FILENAME,没有PATH_INFO值。设置这个参数为cgi.fix_pathinfo=1后,cgi设置完整的路径信息PATH_TRANSLATED的值为SCRIPT_FILENAME,并且设置PATH_INFO信息;如果设为cgi.fix_pathinfo=0则只设置绝对路径PATH_TRANSLATED的值为SCRIPT_FILENAME。cgi.fix_pathinfo的默认值是1。
nginx默认是不会设置PATH_INFO环境变量的的值,需要php使用cgi.fix_pathinfo=1来完成路径信息的获取,但同时会带来安全隐患,需要把cgi.fix_pathinfo=0设置为0,这样php就获取不到PATH_INFO信息,那些依赖PATH_INFO进行URL美化的程序就失效了。
1.可以通过rewrite方式代替php中的PATH_INFO
实例:thinkphp的pathinfo解决方案
设置URL_MODEL=2
location / { if (!-e $request_filename){ rewrite ^/(.*)$ /index.php?s=/$1 last; } }
2.nginx配置文件中设置PATH_INFO值
请求的网址是/abc/index.php/abc
PATH_INFO的值是/abc
SCRIPT_FILENAME的值是$doucment_root/abc/index.php
SCRIPT_NAME /abc/index.php
旧版本的nginx使用如下方式配置
location ~ .php($|/) { set $script $uri; set $path_info ""; if ($uri ~ "^(.+.php)(/.+)") { set $script $1; set $path_info $2; } fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$script; fastcgi_param SCRIPT_NAME $script; fastcgi_param PATH_INFO $path_info; }
新版本的nginx也可以使用fastcgi_split_path_info指令来设置PATH_INFO,旧的方式不再推荐使用,在location段添加如下配置。
location ~ ^.+.php { (...) fastcgi_split_path_info ^((?U).+.php)(/?.+)$; fastcgi_param SCRIPT_FILENAME /path/to/php$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; (...) }
最后可能有人要问为什么apache不会出现这个问题?
apache一般是以模块的方式运行php,apache可以对$_SERVER['PATH_INFO']的值进行设置,不需要另外配置。
一般我们在php中关于url的处理有以下2中方式,已我们熟知的MVC架构为例:
1.通常的路径模式是index.php?c=Controller&a=Action&name=value
2.pathinfo路径模式: index.php/Controller/Action/name/value
为此大家可以使用fastcgi.conf来代替fastcgi.param
location ~ \.php {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
#fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
#fastcgi_pass 127.0.0.1:9000;
fastcgi_pass unix:/dev/shm/php-fpm.socket;
fastcgi_index index.php;
}
这样可以在nginx.conf的server段中直接include它。这个配置文件的重点在fastcgi_split_path_info上,能够处理PATHINFO信息,再通过fastcgi_param设置到位,这样在php当中就能够得到PATHINFO而进行解析。有些朋友使用的是fastcgi_params文件,同样也可以在其前部加入这两句话,效果一样。
这样做之后可以在php.ini中去掉cgi.fix_pathinfo前面的注释,并设置其值为0。PHP默认是启用的,注释的话也是使用默认的启用。避免漏洞,最好关掉
一下引自官方文档解释:
Syntax: | fastcgi_split_path_inforegex; |
---|---|
Default: | — |
Context: | location |
$fastcgi_path_infovariable. The regular expression should have two captures: the first becomes a value of the
$fastcgi_script_namevariable, the second becomes a value of
the
$fastcgi_path_infovariable. For example, with these settings
location ~ ^(.+\.php)(.*)$ { fastcgi_split_path_info ^(.+\.php)(.*)$; fastcgi_param SCRIPT_FILENAME /path/to/php$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info;
and the “
/show.php/article/0001” request, the
SCRIPT_FILENAMEparameter will be equal to “
/path/to/php/show.php”, and the
PATH_INFOparameter will be equal to “
/article/0001”.
相关文章推荐
- (在线文库系统)centos nginx mysql php openoffice pdf2swf pdf2image
- Nginx [error]: invalid PID number "" in "/var/run/
- ubuntu下安装nginx -php
- nginx一个fastcgi的demo错误
- nginx安装lua-nginx-module模块
- 高并发负载均衡(二)——Nginx 和LVS
- Nginx日志切割脚本
- nginx+python+fastcgi环境搭建
- Ubuntu下Nginx多站点配置
- Mac Nginx安装
- Nginx源码分析(1)之——共享内存的配置、分配及初始化
- ubuntu14.04 LEMP(linux+nginx+mysql+php5)构建环境
- Puppet模块(八):keepalived模块
- Puppet模块(七):nginx模块
- Nginx Location配置总结
- zabbix 监控Nginx
- nginx 502错误
- 使用nginx配置Laravel中的虚拟站点
- [nginx] 网上最全面nginx教程(近100篇文章整理)
- centos直接yum安装nginx