PHP 服务器安全和缓解洪水攻击初步总结

编辑于 2017-03-31

* 移动设备下, 可左滑手指以查看较宽代码

写在前面

安全,是个重要但是容易被忽视的问题。以前写过一个简易的论坛,自以为没问题,但还是被发现有 xss 漏洞和 SQL 注入漏洞。

在 Linux 中,当我们首次使用 sudo 执行特权操作的时候,它会有如下提示:

1) Respect the privacy of others.

2) Think before you type.

3) With great power comes great responsibility.

嗯,我们执掌安全问题的时候,必须谨慎,但是防人之心亦不可无。下面就是我的一些初步总结:

网络层面

现在一些主机提供商都有 ddos 防御,有的还不额外收费(其实我也没试过)。还有的提供漏洞扫描(其实没多大用,聊胜于无吧)等功能。这些东西存在一定的误杀,需要手动添加白名单。

好生一个开发者,光靠那些东西肯定是不行的,对于一般的网站,我们可以设置 iptables ,仅开启80端口,具体方法见我的一篇文章《CentOS6.6配置iptables》。

现在有的网站使用了 redismemcached 作为高速缓存,我不建议使用后者。因为后者,

功能少得多,且没有性能的提升

目前没有IP限制,而redis可以限制为localhost

没有身份认证功能

基于安全性和设计考虑,选择redis不错!当然,我们在防火墙里要关闭redis的端口的外网映射 (默认是6379),做到双重防护。

对于展现的信息,诸如php错误提示,和http内发送的服务器程序版本,最好是不要提供。因为特定的版本可能有特定的漏洞。对于 apache ,可以这样设置:

#设置为不显示系统和程序版本信息
ServerToken ProductOnly
ServerSignature Off

#php.ini
expose_php = Off

关于 https

对于安全性要求很高的网站,比如银行等,必须开启全站 https,但依然存在中间人攻击的可能。

文件系统层面

权限的制定有个黄金法则:


最小特权原则



如果php运行在 apache 中,它的默认用户是apache,如果服务器是 nginx ,其默认用户是www。对于php文件,我们就可以设置为755或更小的权限,这样万一遭到脚本攻击,也能尽可能的防止其修改文件。

此外,我们有必要限定php的操作目录,如 apache:

<Directory "/var/www/html/blog/Public">
    php_admin_value open_basedir /var/www/html/blog/Public
    Options FollowSymLinks

# AllowOverride controls what directives may be placed in .htaccess files.
    AllowOverride all
    Order allow,deny
    Allow from all

</Directory>

解释:将 /var/www/html/blog/Public 内的php脚本操作目录限制在这个目录内,并允许目录自身的 .htaccess 重载权限,这样有利于框架发挥更大的安全作用。

对于 Nginx ,这样:

location ^~ /application/common {
                #common/则代码只限定目录,不加/则目录下都不可访问。
                include fastcgi_params;
                fastcgi_param  PHP_VALUE  "open_basedir=/var/www/html/";
        }

另一个很好的安全实践是,在你的 webroot 目录(通常目录名为 "htdocs/"或"html/")下只保留 index.php 文件和 "public" 目录(用于存放 js、css、图片等静态资源)。 只需要这些文件能从 Web 上访问就可以了。

允许你的访问者访问其他位置可能潜在的导致他们访问一些敏感数据或者执行脚本等等。

如果你不允许这样做,你可以使用 .htaccess 文件来限制对这些资源的访问。

很多框架都在每个目录下放置了一个 index.html 文件,试图隐藏这些敏感数据,但是要记住的是,这对于防止一个真正的攻击者来说并不够。

PHP层面

在你的应用程序处理任何数据之前,无论这些数据是来自于提交的表单 POST ,还是来自 COOKIE、URI、XML-RPC ,或者甚至是来自于 SERVER 数组,你都应该使用下面这三步 来处理:

1.验证数据类型是否正确,以及长度、大小等等

2.过滤不良数据

3.在提交到数据库或者显示到浏览器之前对数据进行转义

然后,尽量减少加载的扩展,如posix。其次,可在 php.inidisable_functions 配置项中添加危险的函数:

disable_functions = exec,passthru,popen,proc_open,shell_exec,system,assert,chroot,
scandir,delete,rmdir,rename,chgrp,chmod,chown,copy,mkdir,file,systemlog,ini_after,
ini_get_all,ini_restore

#【注意】实际的配置不要有换行

如果开启了扩展,危险的扩展函数最好也要禁止掉。

对于Nginx服务器,php.ini 中的配置项cgi.fix_pathinfo需要置为0。开启的危害就是假设你的网站有 http://xx.com/a.jpg 这样的一张图片,通过 http://xx.com/a.jpg/foo.php 就可以查看到这个文件的二进制内容,即可以通过 PHP 来执行它。


接下来是SQL注入攻击,触发的方式不少,一般这样预防:

//转义输入的字符串
mysqli_real_escape_strings();
//整形数据就转成整形再写入,避开SQL攻击
intval($str);

或用PDO的 prepare数据绑定 来过滤掉攻击。如果有用框架,使用框架自带的过滤函数,如:

//ThinkPHP 的I方法过滤 $_GET 内的不安全内容。
I('get.xxx',0,'intVal');

也可以使用存储过程,这个不太熟悉,不多说。


· 关于xss攻击,这是个头痛的问题,我们看到discuz论坛有自己的标签而不是html标签,应该主要就是出于此方面考虑吧!所以,评论框可以尽量简洁,在美观性与安全性中做些权衡。

· 对于文件的上传,这里需要严格限制文件类型,并用chmod把控好目录权限。


应该确保对输入数据进行验证过滤。 例如,你希望接受一个数字型的参数,你可以使用 is_numeric() ctype_digit() 或 函数来检查一下。永远将数据限制在你运行的范围内。

记住,不仅要验证 $_POST 和 $_GET 变量,而且也不要放过 cookie 、user-agent 以及 其他所有的不是直接由你的代码生成的数据


接下来是密码的处理,这很重要。

  • 绝不要以明文存储密码

    永远使用 哈希算法 来处理密码。

  • 绝不要使用 Base64 或其他编码方式来存储密码

    这和以明文存储密码是一样的,使用 哈希 ,而不要使用 编码

    编码以及加密,都是双向的过程,而密码是保密的,应该只被它的所有者知道, 这个过程必须是单向的。哈希正是用于做这个的,从来没有解哈希这种说法, 但是编码就存在解码,加密就存在解密。

  • 绝不要使用弱哈希或已被破解的哈希算法,像单纯的 MD5 或 SHA1

    这些算法太老了,而且被证明存在缺陷,它们一开始就并不是为了保存密码而设计的。

    另外,绝不要自己发明算法。

    只使用强密码哈希算法,例如 BCrypt (PHP5.5+) ,在 PHP 自己的密码哈希函数中也是使用它。

  • 绝不要以明文形式显示或发送密码

    即使是对密码的所有者也应该这样。如果你需要 "忘记密码" 的功能,可以随机生成一个新的 一次性的(这点很重要)密码,然后把这个密码发送给用户。

  • 绝不要对用户的密码做一些没必要的限制

    如果你使用除 BCrypt(它有最多 72 字符的限制)之外的其他哈希算法,你应该设置一个相对长一点的密码长度(例如 1024 字符),这样可以缓解 DoS 攻击。

    但是除此之外,对密码的其他限制诸如密码中只允许使用某些字符,或者密码中不允许包含某些字符,就没有任何意义了。

    这样做不仅不会提高安全性,反而 降低了 安全性,而且真的没有任何理由需要这样做。 只要你对密码进行哈希处理了,那么无论是技术上,还是在存储上都没有任何限制。

数据库层面

在设置好 mysql 3306 端口的防火墙规则的同时,必须注意以下问题:

其实有的服务器,用 mysqli_connect();,不带任何参数就能连接上数据库,最好把它删掉:

select user,host from mysql.user;
delete from mysql.user where user='';
#然后【重启】MySQL

另外,数据库密码不要保存在敏感的地方,比如保护相对薄弱的配置文件。

洪水攻击

这几天博客老是抽风,前几天看了下以为是 PHP-fpm 的连接数满了,没怎么管。今天一看日志才知道有人扫,阻塞了 PHP-fpm 程序。好在 Nginx 直接提供针对每个 IP 的访问限制功能,直接配置如下:

http {
    limit_req_zone $binary_remote_addr zone=allips:10m rate=5r/s;
    # 设置一个大小为 10m 的表,存储 IP 地址与访问次数
    # 允许每 IP 每秒 5 次 (rate),并将此规则设为 allips 域

    server {
        limit_req zone=allips burst=5 nodelay;
        # 针对 allips 域,允许 5 × 5(burst,理解为每秒)= 25 次访问
        # 如果超过25次,Nginx 则直接返回 404(或者 503),然后到下一秒重新计时
        # 如果去掉 nodelay 则不会直接返回 404(或者 503),而是阻塞一段时间后再返回页面

        location ~ \.php& {
            ...
            limit_req zone=allips;
            # 对于 PHP 脚本程序(这里会覆盖掉上面的设置),严格按照每 IP 每秒 5 次的限制
        }
    }
}

对 PHP 的限制更严格,一是因为 PHP 文件更耗资源,二是因为一个页面有一个 PHP 文件,但有多个非 PHP 文件(如 js,css),避免误杀。

总结

安全形式向来严峻,大部分都是处理不当造成的。铭记一句话,千里之堤毁于蚁穴。