您的位置:首页 > 运维架构 > Nginx

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>
漏洞同样会出现,如 laruence 所说,实际上这个漏洞和 nginx 真的没什么关系,nginx 只是个 Proxy,它只负责根据用户的配置文件,通过 fastcgi_param 指令将参数忠实地传递给 FastCGI Server,问题在于 FastCGI Server 如何处理 nginx 提供的参数?

比如访问下面这个 URL:

1

[text]
view plain
copy

<span style="font-size: 14px;">http://phpvim.net/foo.jpg/a.php/b.php/c.php</span>

那么根据上面给出的配置,nginx 传递给 FastCGI 的 SCRIPT_FILENAME 的值为:

1

[text]
view plain
copy

<span style="font-size: 14px;">/home/verdana/public_html/unsafe/foo.jpg/a.php/b.php/c.php</span>

也就是 $_SERVER['ORIG_SCRIPT_FILENAME']。

当 php.ini 中 cgi.fix_pathinfo = 1 时,PHP CGI 以 / 为分隔符号从后向前依次检查如下路径:

12
3
4

[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>

直到找个某个存在的文件,如果这个文件是个非法的文件,so… 悲剧了~

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」中定义的:

一旦未来需要加入新的「location」,必然会出现重复定义的「index」指令,这是因为多个「location」是平级的关系,不存在继承,此时应该在「server」里定义「index」,借助继承关系,「index」指令在所有的「location」中都能生效。

参考:Nginx Pitfalls



接下来看看「if」指令,说它是大家误解最深的Nginx指令毫不为过:

很多人喜欢用「if」指令做一系列的检查,不过这实际上是「try_files」指令的职责:

除此以外,初学者往往会认为「if」指令是内核级的指令,但是实际上它是rewrite模块的一部分,加上Nginx配置实际上是声明式的,而非过程式的,所以当其和非rewrite模块的指令混用时,结果可能会非你所愿。

参考:IfIsEvil and How
nginx “location if” works



下面看看「fastcgi_params」配置文件:

Nginx有两份fastcgi配置文件,分别是「fastcgi_params」和「fastcgi.conf」,它们没有太大的差异,唯一的区别是后者比前者多了一行「SCRIPT_FILENAME」的定义:

注意:$document_root 和 $fastcgi_script_name 之间没有 /。

原本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」指令做一次过滤:

参考:Nginx文件类型错误解析漏洞



依照前面的分析,给出一份改良后的版本,是不是比开始的版本清爽了很多:

实际上还有一些瑕疵,主要是「try_files」和「fastcgi_split_path_info」不够兼容,虽然能够解决,但方案比较丑陋,具体就不多说了,有兴趣的可以参考问题描述

补充:因为「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_info 
regex
;
Default:
Context:
location
Defines a regular expression that captures a value for the
$fastcgi_path_info
variable. The regular expression should have two captures: the first becomes a value of the
$fastcgi_script_name
variable, the second becomes a value of
the
$fastcgi_path_info
variable. 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_FILENAME
parameter will be equal to “
/path/to/php/show.php
”, and the
PATH_INFO
parameter will be equal to “
/article/0001
”.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: