经历多年的发展,PHP自身的安全机制也在不断完善,PHP环境部署完成后,通常还会进行一些安全设置。研发人员除熟悉各种PHP漏洞外,还可以通过修改PHP配置文件来加固PHP的运行环境。
当配置好PHP的Web运行环境后,通常需要修改配置来达到安全目的。在优化配置、增强性能的同时,正确地配置PHP可以避免很多安全问题。修改PHP的配置,一般是修改php.ini文件。如果是Windows系统,一般在所安装的PHP目录中可以找到该文件;如果是Linux系统,一般在/etc/php配置路径下可以找到该文件。找到文件所在的位置并打开文件以后,修改对应的选项值,保存文件,然后重启Web运行环境,即可完成修改。
2.1 信息屏蔽
信息屏蔽可以有效地防止服务器信息泄露,避免被恶意攻击者获取服务器信息,为实行下一步攻击做准备。这些信息主要包括服务器信息上的操作系统更新、各种软件信息、PHP版本信息等。
2.1.1 屏蔽PHP错误信息
PHP的错误日志控制项可以控制PHP是否将脚本执行的error、notice、warning日志打印出来。
错误提示信息在研发过程中可以用于辅助研发人员及时发现错误并且进行修复,其中包含了很多服务端的系统信息,但在生产环境中将错误提示信息显示出来是非常危险的。虽然系统在没有漏洞的正常情况下不会出现错误提示信息,但攻击者可能会通过提交非法的参数,诱导服务器进行报错,这样将把服务端的WebServer、数据库、PHP代码部署路径甚至是数据库连接、数据表等关键信息暴露出去。通过对错误信息进行收集和整理,攻击者可以掌握服务器的配置从而更为便利地实施攻击。
如图2-1所示,在配置文件中设置display_errors=On开启了PHP错误显示,在PHP程序遇到错误时,如下的错误信息会被打印在页面上。
图2-1 开启PHP错误显示
这个提示信息暴露了程序和系统的路径,很容易被攻击者利用来了解服务器的目录结构。可以通过修改PHP配置文件将提示信息隐藏,配置文件通常在/etc/php.ini下,具体修改方式如下。
;Default Value: On ;默认开启;Development Value: 0n ;研发环境开启;Production Value: Off ;生成环境关闭;http://php.net/display-errorserror_reporting = E ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATEDdisplay_errors = 0ff ;如果是生成环境,这里应该设定为0ff,避免将错误提示信息展示给用户error_log=/var/1og/php/error_log.log ;指定日志写入路径
在生产环境中,display_errors一般要设置为Off,不要暴露错误信息给用户;研发的时候,可以设置为On。最好的方式是将所有PHP的错误信息记录在日志中,以方便查看。
2.1.2 防止版本号暴露
2015年5月20日,PHP被爆出存在远程DoS漏洞。若攻击者利用该漏洞构造非法请求发起连接,容易导致目标主机CPU被迅速消耗,使服务器宕机,影响正常业务。
漏洞产生的原因是PHP在解析HTTP中的multipart/form-data格式数据时,会不断地重复复制字符串导致DoS。远程攻击者可以通过发送恶意构造的multipart/form-data请求,导致服务器CPU资源被耗尽,从而导致服务器无法响应正常请求。此漏洞涉及众多PHP版本,因而影响范围极大。
受该漏洞影响的PHP版本号如下。
- PHP 5.0.0—5.0.5
- PHP 5.1.0—5.1.6
- PHP 5.2.0—5.2.17
- PHP 5.3.0—5.3.29
- PHP 5.4.0—5.4.40
- PHP 5.5.0—5.5.24
- PHP 5.6.0—5.6.8
这些版本的PHP很容易被攻击者进行DoS攻击。攻击者要利用该漏洞,首先要知道服务器上的PHP版本号。
在默认配置情况下,PHP版本号显示是开启状态,expose_php设置值为On,默认将PHP的版本号返回到HTTP请求的头部信息中,如图2-2所示。
图2-2 PHP版本号显示开启
图2-3所示是一个HTTP请求返回的Response头部数据,HTTP头里返回服务端状态的信息。其中X-Powered-By:PHP/7.2.0的版本号暴露无遗,攻击者很容易捕获此信息。一旦该版本的PHP存在漏洞,攻击者很容易将其利用。
图2-3 HTTP头中显示PHP版本
因此,建议在生产环境中隐藏PHP版本号,在PHP配置文件中查找expose_php,将值设置为Off,PHP的版本显示关闭,如图2-4所示。
隐藏PHP的版本号,可以避免攻击者进行批量扫描,防止服务器暴露,从而降低被攻击的风险。线上环境应该隐藏PHP版本号,通过修改PHP的配置文件,将expose_php的值设置为Off,如图2-5所示。
设置为Off后,PHP会将当前的PHP版本号进行隐藏,防止恶意攻击者通过定位PHP的版本号来利用PHP的固有漏洞。
2.2 防止全局变量覆盖
在PHP全局变量功能开启的情况下,传递过来的数据会被直接注册为全局变量使用,如图2-6所示。在关闭的情况下,PHP会把接收到的数据存放在规定好的全局数组中。
图2-6 PHP全局变量开启
图2-6中将register_globals设置为全局变量开启。接下来用下面的一段代码提交一个用户登录的表单,其中包含用户名和密码。
当register_globals=On时,程序可以直接使用$username和$password来接收值,同时用户也可以定义其他全局变量。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form name="login" action="test_globals.php" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="login">
</form>
</body>
</html>
<?php
echo $username."<br>";
echo $password."<br>";


例如,register_globals配置选项打开之后,可导致下面代码中的$authorized变量被覆盖,无需认证用户名和密码就可以直接设置authorized的值为true,跳过认证进入登录状态,这会造成很大的安全隐患。
<?php
if (authenticated_user()) { //认证用户是否登录
$authorized =true; //authorized变量可以被覆盖
}
如图2-7所示,设置PHP配置文件中register_globals=Off,程序只能使用$_GET[‘username’]、$_GET[‘password’]或$_REQUEST[‘username’]、$_REQUEST [‘password’]来接收传递过来的值。
因此从系统安全角度出发,建议设置register_globals=Off,客户端所有提交到服务端的数据都应该通过PHP预定义内置的全局数组来获取。
图2-7 关闭全局变量
在PHP 5.3之前的版本中,register_globals默认为开启状态。为了防止产生安全隐患,在PHP 5.3中将register_globals设置为关闭状态。在新版的PHP 5.6及PHP 7中,官方已经将register_globals选项去除,以防止全局变量的产生。
2.3 使用PHP的访问限制
PHP可以直接访问本地服务器路径以及在服务器上执行脚本文件。合理地限制PHP的访问范围,可以有效地制止恶意用户对服务器的攻击。
2.3.1 文件系统限制
在PHP中可以通过配置open_basedir来限制PHP访问文件系统的位置,将PHP执行权限限制在特定目录下。当PHP访问服务器的文件系统时,这个设置的位置将被检查。当访问的文件在目录之外时,PHP将拒绝访问。
开启open_basedir可以有效地对抗文件包含、目录遍历等攻击,防止攻击者访问非授权目录文件。
为此选项设置一个值,需要注意的是,如果设置的值是一个指定的目录,则需要在目录最后加上一个“/”,否则会被认为是目录的前缀。
file_get_contents() 函数把整个文件读入一个字符串中。
; open_basedir, if set, limits all file operations to the defined directory
; and below. This directive makes most sense if used in a per-directory
; or per-virtualhost web server configuration file. This directive is
; *NOT* affected by whether Safe Mode is turned On or Off.
; http://php.net/open-basedir
open_basedir = D:\\phpstudy_pro\\WWW
在phpstudy_pro目录下新建一个文件1.txt
<?php
//读取非授权路径
//echo file_get_contents('D:\\phpstudy_pro\\1.txt');
//读取授权路径
echo file_get_contents('test01.php');
2.3.2 远程访问限制
图2-8所示的PHP配置,当PHP的远程访问选项allow_url_fopen开启时,允许PHP系统拥有从远程检索数据的功能,如通过PHP来访问远程FTP或Web,使用file_get_contents()访问远程数据。
图2-8 PHP远程访问开启
下面是示例代码,通过传递统一资源定位符(Uniform Resource Locators,URL)打开远程地址。
<?php
$url = $_GET['url'];
$result=file_get_contents($url);
echo $result;
url:http://localhost/test_fopen.php?url=http://www.baidu.com
图2-9 远程访问开启执行结果
很多研发人员使用这些功能,通过FTP或是HTTP来远程获得数据。然而,这种方法在基于PHP应用程序中会造成一个很大的漏洞。由于部分PHP研发人员缺乏安全认知,在处理用户提交的数据时,没有对恶意用户所提交的数据进行过滤或转码,错误地访问了恶意用户提交的数据中包含的恶意链接,从而将该链接中的攻击代码加载到页面中,导致产生安全漏洞。要解决此问题,需要禁用过程访问。
把allow_url_fopen的值更改为Off将其禁用,再次执行代码,可以看到PHP禁止了远程访问,出现图2-10所示“用户远程访问已经关闭”的提示。
图2-10 远程访问关闭执行结果
2.3.3 开启安全模式
PHP的安全模式是为试图解决共享服务器(shared-server)的安全问题而设立的。开启之后,主要会对系统操作、文件、权限设置等方法产生影响,减少被攻击者植入webshell所带来的某些安全问题,从而在一定程度上避免一些未知的攻击。可以通过在PHP配置文件中修改safemode的值为On来开启PHP安全模式。
启动safe_mode时,会对许多PHP函数进行限制,特别是与系统相关的文件打开、命令执行等函数。所有操作文件的函数将只能操作与脚本UID相同的文件。
如果要将其放宽到GID比较,则设置safe_mode_gid=On可以考虑只比较文件的GID。
设置safe_mode以后,所有命令执行的函数将被限制只能执行php.ini里safe_mode_exec_dir指定目录里的程序,例如使用shell_exec()、exec()等函数执行命令的方式会被禁止。如果确实需要调用其他程序,可以在php.ini中进行如下设置。safe_mode_exec_dir = /usr/local/php/exec
在PHP配置文件中设置选项safe_mode_include_dir,然后复制可执行程序到/usr/local/php/exec目录,这个目录中的可执行程序不受UID/GID检查约束,PHP脚本就可以用shell.exec()、exec()、stustem()等函数来执行这些程序。而且该目录里的Shell脚本还可以调用其他目录里的系统命令。
从PHP 4.2.0版本开始,safe_mode_exec_dir参数可以接受以目录格式字符串为前缀的匹配方式。指定的限制实际上是一个前缀,而非一个目录名称。例如,在系统中如果定义“safe_mode_include_dir =/dir/incl”,字符串将允许访问“/dir/include”和“/dir/inclouds等以“/dir/ind”开头的目录路径。如果希望将访问控制在一个指定的目录中,则应在结尾加上一个斜线。safe_mode_exec_dir = /dir/incl/
当启用安全模式时,可以通过PHP设置选项safe_mode_allowed_env_vars来设置哪些系统环境变量可以被修改,用户只能改变在这里提供的前缀的环境变量。默认情况下,用户只能设置以PHP开头的环境变量(例如PHP_FOO = BAR)。
注意,safe_mode_allowed_env_vars设置项为空,PHP将使用户可以修改任何环境变量。
PHP的safe_mode_protected_env_vars设置项,包含由一个逗号分隔的环境变量的列表,最终用户不能用putenv()来改变这些环境变量,甚至在safe_mode_allowed_env_vars中设置了允许修改时也不能改变这些变量。
虽然PHP的安全模式不是万能的,但还是强烈建议打开安全模式,这样能在一定程度上避免一些未知的攻击。不过在启用安全模式后会有很多限制,可能对应用带来影响,所以还需要通过调整代码和系统配置来综合考虑。更多被安全模式限制或屏蔽的函数,可以参考PHP官方手册。
2.3.4 禁用危险函数
PHP配置文件中的disable_functions选项能够在PHP中禁用指定的函数。PHP中有很多危险的内置功能函数,如果使用不当,可能造成系统崩溃。禁用函数可能会为研发带来不便,但禁用的函数太少又可能增加研发人员写出不安全代码的概率,同时为攻击者非法获取服务器权限提供便利。
在PHP配置文件中添加需要禁用的函数可以有效地避免webshell,如下所示就是在PHP配置中添加了多个常用的禁用函数。
表2-1所列是一些建议禁用的函数,要尽量避免使用这些函数,防止给系统留下隐患。
表2-1 建议禁用的函数
2.4 PHP中的Cookie安全
在Web系统中,Cookie中常常包含重要的服务器会话信息以及与用户相关的各种私密信息。在整个安全传输过程中要特别重视Cookie的安全,避免被恶意用户截获以及利用。
2.4.1 Cookie的HttpOnly
HttpOnly可以让Cookie在浏览器中不可见,开启HttpOnly可以防止脚本通过document对象获取Cookie。
浏览器在浏览网页时一般不受任何影响,Cookie会被放在浏览器头中发送出去(包括Ajax时),应用程序一般是不会在JS里操作这些敏感Cookie的。对于一些敏感的Cookie一般采用HttpOnly,对于一些需要在应用程序中用JS操作的Cookie就不予设置,这样就保障了Cookie信息的安全,也保证了应用,可以有效地预防一些XSS和CSRF攻击。此外,需要在PHP配置文件中设置HttpOnly开关,将session.cookie_httponly的值设置为1表示开启HttpOnly。配置方式如下。
session.cookie_httponly = 1;开启HttpOnly
2.4.2 Cookie的Secure
如果Web传输协议使用的是HTTPS,则应开启cookie_secure选项,当Secure属性设置为true时,Cookie只有在HTTPS下才能上传到服务器,而在HTTP下是没法上传的。防止Cookie被窃取,需要在PHP配置中修改,将session.cookie_secure的值设置为1,标示开启Secure。配置方式如下。
session.cookie_secure = 1
2.4.3 指定Cookie的使用范围
Cookie一定要设置超时和Domain,敏感信息尽量不要保存在Cookie中,Cookie中的数据尽量进行加密,设置domain时尽量不要设置*.ptpress.com.cn之类通配域名,以避免其他同根域网站的XSS漏洞引起的跨站Cookie窃取,PHP中使用setcookie()函数进行Cookie的设置。在下面的代码中,name是Cookie的名称,value是Cookie的值,expire是失效时间,path是Cookie的生效路径,domain是Cookie的作用域名范围,secure用于指定是否开启HTTPS连接来传输Cookie。
setcookie(name,value,expire,path,domain,secure)
2.5 PHP的安装与升级
PHP脚本通过PHP解析器解析进行执行,PHP解析器本身会存在安全漏洞,合理地安装使用PHP解析器可以提高系统的安全性。
当PHP各版本中发现存在安全漏洞时,PHP官方会及时发布安全补丁来修复漏洞,应该时刻关注PHP官方发布的最新发版。表2-2所列是各个PHP版本的CVE[插图]漏洞数量。在每个版本中官方都会及时发布新版来修复安全漏洞,要对PHP及时升级,降低系统的安全隐患。
表2-2 PHP各版本的CVE漏洞数量
在PHP 4、PHP 5这些历史版本中由于支持不安全的MySQL函数,如果研发人员不重视安全问题,任意拼接字符串,那么会经常性地引起SQL注入漏洞。
截止到2018年底最新的版本是PHP 7。在PHP 7中,有几个比较重大的改动,这些改动对于提高应用安全性有很大帮助。
(1)PHP 7中移除了一些不安全的函数。如移除了对于MySQL函数的支持,MySQL函数在许多情况下是不安全的,经常由于使用不当而造成SQL注入;移除了对ereg函数的支持,ereg函数存在%00截断漏洞,导致了正则过滤被绕过。
(2)在PHP 7中,password_hash()函数的盐(salt)选项被弃用,因为研发人员会生成自己的salt,通常自己生成的salt是不安全的。该功能本身提供salt的加密安全,函数内部默认带有salt能力,无须研发人员提供salt。(3)capture_session_meta函数中的SSL上下文选项被弃用,PHP 7中通过stream_get_meta_data()函数使用SSL元数据。
(4)PHP 7中允许在代码中增加标量类型说明,有效地防止了因数据转换造成的安全隐患。PHP 7中标量类型声明有如下两种模式。
强制模式:强制模式是默认模式,不需要指定。强制模式代码示例如下。
<?php // declare(strict_types=1); function sum(int ...$ints){ return array_sum($ints); } print(sum(6,'6',6.1)); // 上述强制模式执行代码输出“18”。严格模式:严格模式必须明确标明。给每一个PHP文件,添加一个新的可选指令“declare(strict_types=1);”可让同一个PHP文件内的全部函数调用和语句返回,都有一个“严格约束”的标量类型声明检查,且必须按指定变量类型来进行赋值,使变量传递变得更加安全,否则PHP会抛出错误异常。下面代码是严格模式的示例。
<?php declare(strict_types=1); function sum(int ...$ints){ return array_sum($ints); } print(sum(6,'6',6.1)); //由于传入参数与定义参数的数据类型不一致,因此PHP抛出异常。(5)在PHP 7版本中使用了更安全的随机数生成器,添加了更好的随机数random_int()、random_bytes(),并用其代替PHP 5的mt_rand()。代码示例如下。
<?php var_dump(random_int(100,999)); var_dump(random_int(-1000,0));random_bytes()函数返回string类型,并接收一个int类型为参数,该参数规定了所返回字符串的字节长度。代码示例如下。
<?php $bytes = random_bytes(5); //指定5字节长度 var_dump(bin2hex($bytes)); //输出string(10) "87cf51115d"2.5.1 尽量减少非必要模块加载
加载尽量少的模块在优化PHP性能的同时增加了安全性,使用php -m命令可以查看当前PHP加载的模块。
例如,如果用不到Redis或者ImageMagick,则完全可以将其禁用,以避免不必要的漏洞引起的安全问题。如下在配置文件中添加分号(;)将所在模块行注释化,PHP在启动后就不会加载Redis和ImageMagick模块。
2.5.2 使用第三方安全扩展
Suhosin是PHP项目的一个保护系统,它的设计初衷是为了保护服务器和用户抵御PHP项目和PHP核心中已知或者未知的缺陷。
Suhosin有两个独立的部分,可以分开使用或者联合使用。第一部分是一个用于PHP核心的补丁,它能抵御缓冲区溢出或者格式化串的弱点;第二部分是一个强大的PHP扩展,它包含其他所有的保护措施。更多的信息可以到Suhosin的官方网站进行学习。
Taint是一个用于检测xss/sqli/shell注入的PHP扩展模块,用来监测来自GET、POST、Cookie中的数据。这些从客户端接收到的数据如果没有经过过滤或转义处理而被服务端直接使用,Taint会抛出安全提示信息来警示研发人员。

