给网站戴上安全帽

网站的安全性总是那么让人费神,一不小心就被攻击了,比如 SQL 注入,挂马,XSS 攻击,CSRF 攻击,请求劫持等等。本文将针对 lnmp 架构网站的安全性提高提供一些简单的有效措施来防范。

使用 https 防止请求劫持

我们有时打开网站可能看到莫名其妙的出现了广告,而这些广告不是自己插入的,并且线上代码也完好。那么这种情况很有可能是因为请求被运营商劫持并且对返回数据动了手脚。这种问题 BAT 也有遇到。而解决这个问题最好的办法就是使用 https 替换 http,这样他们就没办法做这种恶心的事了。
https 是 http 下加入了一层 SSL,从而提高安全性,https 也是现在业界提倡的做法,网站使用 https 门槛也越来越低了。比如 StartSSL[https://www.startssl.com] 可提供受浏览器信任的免费的 SSL 证书。

参数验证和转义

我们说永远不要相信客户端,做一个安全性好的网站就是相信别人都是不可信的。对于防止 sql 注入、xss 攻击来说参数验证和转义是必不可少的工作。以下是一些 php 在做参数验证的 tips。

  1. 对于明确是整形的参数做一次强制类型转换比如 (int)$_GET['id'] 或者 $id=$_GET['id'] + 0,浮点型类似做法。
  2. 对于是字符串类型参数则进行一次转义,可以使用 php 函数 htmlspecialchars 对单引号、双引号、反斜线、左右尖括号等这些对 sql 注入、xss 攻击敏感的字符进行转义。也可以自己写一个函数对这些敏感字符进行替换,比如:
 # 转义 encode:
 $input = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4})|[a-zA-Z][a-z0-9]{2,5});)/', '&\\1',
                str_replace(array('&', '"', '<', '>', "'"), array('&', '&quot', '&lt', '&gt', '&#039'), $input));

 # 转义 decode:
 $input = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4})|[a-zA-Z][a-z0-9]{2,5});)/', '&\\1',
                str_replace(array('&', '&quot', '&lt', '&gt', '&#039'), array('&', '"', '<', '>', "'"), $input));
  1. 参数绑定是防止 sql 注入的最安全有效做法,并且使用 sql 预处理还能提高 sql 执行效率。
  2. 执行业务代码前对请求参数做统一验证
    对于防止 sql、xss 攻击来说,一种做法是在业务代码执行前执行一次统一的参数验证。将可能导致 sql、xss 攻击的请求拒绝掉。比如在项目入口文件前面执行以下代码:
 <?php
 /**
  * URL 请求过滤
  */
 $str_request_method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '';
 $str_request_cookie = isset($_SERVER['HTTP_COOKIE']) ? $_SERVER['HTTP_COOKIE'] : '';

 // cookie 拦截规则
 $cookiefilter = "\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";

 if ($str_request_method == 'POST') {
    // post 拦截规则
    $str_request_filter = "\\b(alert\\(|confirm\\(|prompt\\()\\b|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b[^>]*?>|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
    fn_filter_request($_POST, $str_request_filter);
} else {
    // get 拦截规则
    $str_request_filter = "\\b(alert\\(|confirm\\(|prompt\\()\\b|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b[^>]*?>|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
    fn_filter_request($_GET, $str_request_filter);
 }

 function fn_filter_request($arr_request, $str_request_filter)
 {
    foreach ($arr_request as $key => $val) {
        if (is_array($val)) {
            fn_filter_request($val, $str_request_filter);
        } else {
            $bool_match_result = preg_match('/' . $str_request_filter . '/is', $val, $arr);

            if ($bool_match_result) {
                echo 'URL请求非法!';
                die();
            }
        }
    }
 }

这种做法简单但是会损失一部分性能,特别是在 post 参数较大情况。

防止项目文件被恶意访问

在生产环境,php 项目一般是所有 php 文件放到一起。这样如果非入口文件和其他文件都在 nginx 中配置成了可访问,那么可能造成恶意调用。其实可以将项目中可访问的文件单独放到一个目录中比如命名为 public。然后 nginx 的 root 配置项配置为:

#比如 root 目录配置成:
root /*/*/public/ 

这种做法比起通过定义常量再每个文件检查是否定义了常量做法来说更简单安全有效,不侵入业务代码。
如果是单入口文件比如只有 index.php 文件那么还可以在 nginx 中配置:

location ~ \.php$ {
    ...
        fastcgi_param  SCRIPT_FILENAME  $document_root$index.php; # 指定 php 文件只可以访问 index.php
    ...
}

.svn、.git 泄露源代码

这个是包括有些大互联网公司都可能犯得错误,将 .svn,.git 目录文件暴露给攻击者,从而泄露源代码。可以通过 nginx 简单配置下来避免这个漏洞。

server {
    ...
    location ~ /\.git/ {
        return 404;
    }
    location ~ /\.svn/ {
        return 404;
    }
    ...
}

nginx、php 版本隐藏

一些 php、nginx 的版本可能存在被发现的漏洞。如果隐藏掉版本号也减少了相应的风险。
nginx 隐藏版本号:

第一步:

#找到 nginx.conf 文件并做以下修改
http {
    ...
    server_tokens off; # 此处改为off即可
    ...
}

第二步:

#修改 fastcgi.conf 或 fcgi.conf 配置文件(这个配置文件名也可以自定义的,根据具体文件名修改)
#找到:
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
#改为:
fastcgi_param SERVER_SOFTWARE nginx; 

php 隐藏版本号:

;修改 php.ini 文件
expose_php = Off

参数签名校验

这种做法很常见,比如在使用第三方 SDK 时经常会要求对参数进行一次签名。我们在开发比如移动端接口时就可以这样做。
具体流程:
客户端对所有要提交的参数串在一起再拼接一个和服务端约定好的秘钥进行一次 md5 加密签名,然后服务端也对除了签名参数外的其他参数按照约定好的规则串起来再拼接一个和客户端约定好的秘钥进行一次 md5 加密得到签名,最后两个签名进行比较是否一致就完成了整个签名校验工作。

做了签名校验就较少了中途参数被篡改的可能性,这个在抽奖、投票活动中尤为重要。

防止挂马

有时我们会看到自己网站被贴上一些“狗皮膏药”,代码中被植入了恶意代码。这种时候就说明我们的源代码被篡改了。做好防挂马其实就是要防止代码被修改。防止挂马有以下几个策略:

  1. 管理好目录文件权限,php、nginx 使用一个独立的所有者比如 worker,源代码所有者可以是 root,目录是755,文件是644权限。这样不至于在请求时参数中带有恶意命令并被执行了从而篡改源文件。
  2. 对 php 危险函数禁用,如 passthru,exec,assert,system,chroot,chgrp,chown;
  3. 对上传文件进行 mime 检验,并且设置 mime 类型白名单;
  4. 定期更改服务器或管理后台密码,保管好密码不至于被泄露,使用复杂不可记忆密码;

Cookie 安全

对 Cookie 添加 HttpOnly 属性,实现禁止 JavaScript 访问到该 Cookie,这样可以防止 XSS 攻击者窃取该 Cookie 信息,对于一些非常敏感的比如用户 token 信息这样做是有必要的。

PHP 错误提示

在正式生产环境中应该将错误信息不被向外暴露,因为错误信息可能会给攻击者提供一些敏感信息,从而利于进行有针对性的攻击。

更安全地存储信息

将用户的敏感信息比如密码、邮箱、身份证号码独立存储在一张表中,独立于普通的用户信息表。这样可以防止因为不小心 Select * 并且向客户端吐出了未过滤的敏感信息。

密码应该使用不可逆加密算法来加密存储,但不可以只简单地进行一次 MD5 加密这样不安全。可以对密码进行加 salt 加密或者加 key 加密来提高密码的安全性,这样就算密码被泄露也不至于产生太严重的后果。

<?php
// 一个加Key加密方法
function selfMD5($str, $key = 'SELFKEY') 
{
   if ($key == '') {
       return false;
   }
   
   $keyLen = strlen($key);
   $md5 = md5($str);
   $len = strlen($md5);
   $len = $len / 2;
   $outStr = '';
   for ($i = 0; $i < $len; $i ++) {
       $ch1 = hexdec($md5[$i * 2] . $md5[$i * 2 + 1]);
       $ch2 = ord($key[$i % $keyLen]);
       $ch = ($ch1 ^ $ch2);
       
       $outStr .= $ch <= 0xF ? ('0' . dechex($ch)) : dechex($ch);
   }
   
   return $outStr;
}

身份证等敏感信息存储可以使用一些对称加密算法来提高安全性,如 3DES、AES 等来提高安全性。

添加新评论