自动检测 PHP-FPM 的错误并重启的 PHP 脚本

公司的 WEB 生产服务器使用 NGINX+PHP-FPM 构建。

近日 NGINX 频报 (110: Connection timed out) 以及 (11: Resource temporarily unavailable) 的错误,出错后后端的 PHP-FPM 几乎全部挂死,重启 PHP-FPM 后又能正常工作。

初步认定是 PHP-FPM 或系统参数配置有问题,优化了系统参数并重启服务器后,目前尚未发现问题。

但是依然比较担心意外情况发生,没人想在春节期间再来照看这些服务器,索性用 PHP 写了个脚本监控错误日志,监测到错误后自动重启 PHP-FPM。

脚本下载:
phpfpm_guarder

配置说明:
$bin_tail_path tail命令路径
$bin_cp_path cp命令路径
$log_path 错误日志文件路径
$error_logs 要检测的错误类型,可以检测多种
$restart_cmd 重启 PHP-FPM 的命令
$guide_period 检测周期,单位为秒
$max_error_cnt 在检测周期里面发现多少次错误后重启服务器

没几天就要过年了,祝各位新快乐,万事如意!

— EOF —

Nginx+PHP 配置漏洞:静态文件都可以当作 PHP 解析

漏洞危险等级:毁灭性。

这个漏洞严格上说并不是 Nginx 和 PHP 本身的漏洞造成的,而是由配置造成的。在我之前写的许多配置中,都普遍存在这个漏洞。

简易检测方法:
打开 Nginx + PHP 服务器上的任意一张图片,如:
http://hily.me/test.png
如果在图片链接后加一串 /xxx.php (xxx为任意字符)后,如:
http://hily.me/test.png/xxx.php
图片还能访问的话,说明你的配置存在漏洞。

漏洞分析:

下面通过分析一个很常见的 Nginx 配置来解释下漏洞的成因:

server {
    listen       80;
    server_name  test.local;

    access_log  /work/www/logs/test.access.log  main;
    error_log  /work/www/logs/test.error.log;

    location / {
        root   /work/www/test;
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        root           /work/www/test;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_pass   unix:/tmp/php-fpm.sock;
    }
}

我们在 /work/www/test/ 目录下新建一个文件 test.png,内容如下:

那么访问 http://test.local/test.png 时,输出为文本内容:

但是当在后面加上 /xxx.php 时,即 http://test.local/test.png/xxx.php,可怕的事情发生了:

Array
(
    [HOSTNAME] => 
    [PATH] => /usr/local/bin:/usr/bin:/bin
    [TMP] => /tmp
    [TMPDIR] => /tmp
    [TEMP] => /tmp
    [OSTYPE] => 
    [MACHTYPE] => 
    [MALLOC_CHECK_] => 2
    [USER] => www
    [HOME] => /home/www
    [FCGI_ROLE] => RESPONDER
    [SCRIPT_FILENAME] => /work/www/test/test.png
    [QUERY_STRING] => 
    [REQUEST_METHOD] => GET
    [CONTENT_TYPE] => 
    [CONTENT_LENGTH] => 
    [SCRIPT_NAME] => /test.png/xxx.php
    [REQUEST_URI] => /test.png/xxx.php
    [DOCUMENT_URI] => /test.png/xxx.php
    [DOCUMENT_ROOT] => /work/www/test
    [SERVER_PROTOCOL] => HTTP/1.1
    [GATEWAY_INTERFACE] => CGI/1.1
    [SERVER_SOFTWARE] => nginx/0.7.62
    [REMOTE_ADDR] => 192.168.1.163
    [REMOTE_PORT] => 4080
    [SERVER_ADDR] => 192.168.1.12
    [SERVER_PORT] => 80
    [SERVER_NAME] => test.local
    [REDIRECT_STATUS] => 200
    [HTTP_ACCEPT] => image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/QVOD, application/QVOD, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
    [HTTP_ACCEPT_LANGUAGE] => zh-cn
    [HTTP_ACCEPT_ENCODING] => gzip, deflate
    [HTTP_USER_AGENT] => Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQPinyin 689; QQDownload 627; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.2; TheWorld)
    [HTTP_HOST] => test.local
    [HTTP_CONNECTION] => Keep-Alive
    [ORIG_SCRIPT_FILENAME] => /work/www/test/test.png/xxx.php
    [PATH_TRANSLATED] => /work/www/test
    [PHP_SELF] => /test.png/xxx.php
    [REQUEST_TIME] => 1274125615
)

环境变量中,SCRIPT_FILENAME 是 Nginx 传过来的:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

$fastcgi_script_name 变量说明请参考:
http://wiki.nginx.org/NginxHttpFcgiModule

Nginx 传给 PHP 的值为 /work/www/test/test.png/xxx.php,即 $_SERVER 中 ORIG_SCRIPT_FILENAME 的值,但是 $_SERVER 中 SCRIPT_FILENAME 却是 /work/www/test/test.png。

原因是,/work/www/test/test.png/xxx.php 并不存在,对于这些不存在的路径,PHP 会检查路径中存在的文件,并将多余的部分当作 PATH_INFO。
这里,/work/www/test/test.png 被 PHP 解析为 SCRIPT_FILENAME,/xxx.php 被 PHP 解析为 PATH_INFO 后被丢弃,因此并没有在 $_SERVER 中出现。

解决方法:

解决这个漏洞的方法很显然:关闭上面所述的解析即可。

这个解析可以在 PHP 的配置文件中设置,默认为开启。在这里我们需要将它关闭:

; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP’s
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting
; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
; http://php.net/cgi.fix-pathinfo
;cgi.fix_pathinfo=1
cgi.fix_pathinfo=0

其中 cgi.fix_pathinfo=0 为新增的配置行,表示关闭 PHP 的自动 PATH_INFO 检测。关闭后,该配置漏洞即可消除。

更好的解决方案?
以上方案并不是最完美的,如果你先前有用到 cgi.fix_pathinfo 这个特性,影响会很大,比如关闭后,我的 Blog(Wordpress)文章的 URL 目录形式就得用 rewrite 来实现了。
如果可以将 PHP 设置成只解析 .php 为扩展名的文件,那么这个问题解决起来会更合理。
不过我没找到相关的设置项,或许今后应该出现在 php-fpm 的配置文件中?

总结:
这类问题基本上是无法预料的,但是如果架构设计良好的话,即使存在这个问题,也不会影响安全性。这里给出架构上的安全建议:
* 尽可能使动静内容分离,所有的静态内容存在于静态内容服务器,静态内容服务器上不解析PHP,这样静态文件就永远不能被解析了。

— EOF —

Nginx >= 0.7.31 已经支持 PATH_INFO

前一段时间为了 PATH_INFO 问题搞得郁闷,来回忆下原来的配置(以CodeIgniter框架的配置为例):

server {
	listen       80;
	server_name  test.local;

	location / {
		root   /www/test;
		index  index.html index.htm index.php;
		rewrite ^/$ /index.php last;
		rewrite ^/(?!index\.php|robots\.txt|images|js|styles)(.*)$ /index.php/$1 last;
	}

	location ~ \.php($|/) {
		root	   /www/test;

		set $script     $uri;
		set $path_info  "";

		if ($uri ~ "^(.+?\.php)(/.*)$") {
		    set $script     $1;
		    set $path_info  $2;
		}

		fastcgi_index	index.php;
		fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
		fastcgi_param   PATH_INFO       $path_info;
		fastcgi_pass    unix:/tmp/php-fpm.sock;
		include	fastcgi_params;
    	}
}

虽然可以实现 PATH_INFO 了,但是还是有缺陷,URL 中的中文不会被 urldecode。

好在,0.7.31 版本以上的 Nginx 新增了fastcgi_split_path_info 这个指令,现在配置起来清晰多了:

server {
	listen       80;
	server_name  test.local;

	location / {
		root   /www/test;
		index  index.html index.htm index.php;
		rewrite ^/$ /index.php last;
		rewrite ^/(?!index\.php|robots\.txt|images|js|styles)(.*)$ /index.php/$1 last;
	}

	location ~ ^(.+\.php)(.*)$ {
		root	   /www/test;

		fastcgi_index	index.php;
		fastcgi_split_path_info ^(.+\.php)(.*)$;
		fastcgi_param	SCRIPT_FILENAME	$document_root$fastcgi_script_name;
		fastcgi_param	PATH_INFO		$fastcgi_path_info;
		fastcgi_param	PATH_TRANSLATED	$document_root$fastcgi_path_info;
		fastcgi_pass    unix:/tmp/php-fpm.sock;
		include	fastcgi_params;
    }
}

指令的具体帮助请参考官方WIKI:

http://wiki.nginx.org/NginxHttpFcgiModule#fastcgi_split_path_info

— EOF —

gentoo 下 svn 服务器搭建

无废话安装:

emerge apr

emerge apr-util

emerge apache

USE=”apache2″ emerge subversion

创建仓库:

svnadmin create /svn/test

apache 配置:

Listen 88

NameVirtualHost *:88

DocumentRoot “/svn/test”

<Directory “/svn”>

Options None

Order allow,deny

Allow from all

</Directory>
<VirtualHost *:88>

ServerName test.svn

<Location /test>

DAV svn

SVNPath /svn/test

AuthType Basic

AuthName “SubVersion Login”

AuthUserFile /etc/svn/passwd

Require valid-user

</Location>

</VirtualHost>

apache 启动时提示错误:

# /etc/init.d/apache2 start

* Caching service dependencies … [ ok ]

* apache2 has detected an error in your setup:

apache2: Syntax error on line 135 of /etc/apache2/httpd.conf: Cannot load /usr/lib64/apache2/modules/mod_dav_svn.so into server: /usr/lib64/apache2/modules/mod_dav_svn.so: undefined symbol: dav_register_provider

修正方法:

# vim/etc/conf.d/apache2

在 APACHE2_OPTS 后面加入 -D DAV -D DAV_FS -D SVN -D AUTHZ_SVN

nginx前端配置:

server {

listen       80;

server_name svn.local;

charset      utf-8;

access_log      /work/www/log/svn.access_log main;

error_log       /work/www/log/svn.error_log error;

location / {

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-Proto https;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_pass              http://svn.local:88;

}

}

创建身份验证文件:

htpasswd -c /etc/svn/passwd hilyjiang

登录 http://svn.local/test 输入密码后即可访问SVN。

— EOF —

Nginx + PHP-FPM (11: Resource temporarily unavailable)

今天在测试服务器上搭了 Nginx + PHP-FPM 的环境,结果发现 PHP 页面频繁出现 502 Bad Gateway 错误。
Nginx 版本:nginx/0.7.61
PHP-FPM:php-5.3.0-fpm-0.5.12
Nginx 错误日志:

connect() to unix:/tmp/php-fpm.socket failed (11: Resource temporarily unavailable) while connecting to upstream

看起来似乎是 PHP-FPM 的问题,看老外的讨论:
http://forum.nginx.org/read.php?3,31467,31467

原因是 PHP-FPM 在 backlog 设置为 -1 的情况下,并没有使用系统的 backlog 设置。
所以我们需要显式指定 backlog 参数。

把 PHP-FPM 配置文件中的:

<value name=”backlog”>-1</value>

改成:

<value name=”backlog”>1024</value>

问题消除!

— EOF —