Nginx-配置误区

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
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;
}

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

比如访问下面这个 URL:

1
[text] view plain copy
  1. http://phpvim.net/foo.jpg/a.php/b.php/c.php  

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

1
[text] view plain copy
  1. /home/verdana/public_html/unsafe/foo.jpg/a.php/b.php/c.php  

也就是 $_SERVER['ORIG_SCRIPT_FILENAME']。

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

1
2
3
4
[text] view plain copy
  1. /home/verdana/public_html/unsafe/foo.jpg/a.php/b.php/c.php  
  2. /home/verdana/public_html/unsafe/foo.jpg/a.php/b.php  
  3. /home/verdana/public_html/unsafe/foo.jpg/a.php  
  4. /home/verdana/public_html/unsafe/foo.jpg  

直到找个某个存在的文件,如果这个文件是个非法的文件,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的二进制内容:

  1.  
  2. http://www.laruence.com/fake.jpg/foo.php

为什么会这样呢?

比如, 如下的nginx conf:

  1. location ~ \.php($|/) {
  2.      fastcgi_pass 127.0.0.1:9000;
  3.      fastcgi_index index.php;
  4.  
  5.      set $script $uri;
  6.      set $path_info "";
  7.      if ($uri ~ "^(.+\.php)(/.*)") {
  8.           set $script $1;
  9.           set $path_info $2;
  10.      }
  11.  
  12.      include fastcgi_params;
  13.      fastcgi_param SCRIPT_FILENAME $document_root$script;
  14.      fastcgi_param SCRIPT_NAME $script;
  15.      fastcgi_param PATH_INFO $path_info;
  16. }

通过正则匹配以后, SCRIPT_NAME会被设置为”fake.jpg/foo.php”, 继而构造成SCRIPT_FILENAME传递个PHP CGI, 但是PHP又为什么会接受这样的参数, 并且把a.jpg解析呢?

这就要说到PHP的cgi SAPI中的参数, fix_pathinfo了:

  1. ; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's
  2. ; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
  3. ; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
  4. ; this to 1 will cause PHP CGI to fix it's paths to conform to the spec. A setting
  5. ; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
  6. ; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
  7. cgi.fix_pathinfo=1

如果开启了这个选项, 那么就会触发在PHP中的如下逻辑:

  1. /*
  2. * if the file doesn't exist, try to extract PATH_INFO out
  3. * of it by stat'ing back through the '/'
  4. * this fixes url's like /info.php/test
  5. */
  6. if (script_path_translated &&
  7.      (script_path_translated_len = strlen(script_path_translated)) > 0 &&
  8.      (script_path_translated[script_path_translated_len-1] == '/' ||
  9. ....//以下省略.

到这里, PHP会认为SCRIPT_FILENAME是fake.jpg, 而foo.php是PATH_INFO, 然后PHP就把fake.jpg当作一个PHP文件来解释执行… So…

这个隐患的危害用小顿的话来说, 是巨大的.


对很多人而言,配置Nginx+PHP无外乎就是搜索一篇教程,然后拷贝粘贴。听上去似乎也没什么问题,可惜实际上网络上很多资料本身年久失修,漏洞百出,如果大家不求甚解,一味的拷贝粘贴,早晚有一天会为此付出代价。

 

假设我们用PHP实现了一个前端控制器,或者直白点说就是统一入口:把PHP请求都发送到同一个文件上,然后在此文件里通过解析「REQUEST_URI」实现路由。

此时很多教程会教大家这样配置Nginx+PHP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
     listen 80;
     server_name foo.com;
 
     root /path;
 
     location / {
         index index.html index.htm index.php;
 
         if (!-e $request_filename ) {
             rewrite . /index.php last;
         }
     }
 
     location ~ \.php$ {
         include fastcgi_params;
         fastcgi_param SCRIPT_FILENAME /path $fastcgi_script_name ;
         fastcgi_pass 127.0.0.1:9000;
         fastcgi_index index.php;
     }
}

这里面有很多错误,或者说至少是坏味道的地方,大家看看能发现几个。

我们有必要先了解一下Nginx配置文件里指令的继承关系:Nginx配置文件分为好多块,常见的从外到内依次是「http」、「server」、「location」等等,缺省的继承关系是从外到内,也就是说内层块会自动获取外层块的值作为缺省值(有例外,详见参考)。

参考:UNDERSTANDING THE NGINX CONFIGURATION INHERITANCE MODEL

让我们先从「index」指令入手吧,在问题配置中它是在「location」中定义的:

1
2
3
location / {
     index index.html index.htm index.php;
}

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

参考:Nginx Pitfalls

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

1
2
3
if (!-e $request_filename ) {
     rewrite . /index.php last;
}

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

1
try_files $uri $uri / /index.php;

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

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

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

1
include fastcgi_params;

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

1
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

注意:$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」指令做一次过滤:

1
try_files $uri =404;

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
     listen 80;
     server_name foo.com;
 
     root /path;
     index index.html index.htm index.php;
 
     location / {
         try_files $uri $uri / /index.php;
     }
 
     location ~ \.php$ {
         try_files $uri =404;
 
         include fastcgi.conf;
         fastcgi_pass 127.0.0.1:9000;
     }
}

实际上还有一些瑕疵,主要是「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]
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]
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”.


你可能感兴趣的:(Nginx-轻量级服务器)