1、把握整站的构造,避免透露站点敏感目录
在写代码之初,我也是像很多老源码一样,在根目录下放上index.php、register.php、login.php,用户点击注书页面,就跳转到http://localhost/register.php。并没有太多的构造的思想,像这样的代码构造,最大的问题倒不是安全性问题,而是代码扩展与移植问题。
在写代码的过程中,我们常要对代码进行修正,这时候如果代码没有统一的一个入口点,我们可能要改很多地方。后来我读了一点emlog的代码,创造网站真正的前端代码都在模板目录里,而根目录下就只有入口点文件和配置文件。这才顿悟,对全体网站的构造进行了修正。

网站根目录下放上一个入口点文件,让它来对全体网站所有页面进行管理,这个时候注书页面变成了http://localhost/?act=register,任何页面只是act的一个参数,在得到这个参数后,再用一个switch来选择要包含的文件内容。在这个入口点文件中,还可以包含一些常量的定义,比如网站的绝对路径、网站的地址、数据库用户密码。往后我们在脚本的编写中,只管即便利用绝对路径而不要利用相对路径(否则脚本如果改变位置,代码也要变),而这个绝对路径就来自入口点文件中的定义。
当然,在安全性上,一个入口点文件也能隐蔽后台地址。像这样的地址http://localhost/?act=xxx不会暴露后台绝对路径,乃至可以常常变动,不用改变太多代码。一个入口点文件也可以验证访问者的身份,比如一个网站后台,不是管理员就不许可查看任何页面。在入口点文件中就可以验证身份,如果没有登录,就输出404页面。
有了入口点文件,我就把所有非入口点文件前面加上了这句话:
<?php
if(!defined('WWW_ROOT'))
{
header(\"大众HTTP/1.1 404 Not Found\"大众);
exit;
}
?>
WWW_ROOT是我在入口点中定义的一个常量,如果用户是通过这个页面的绝对路径访问(http://localhost/register.php),我就输出404缺点;只有通过入口点访问(http://localhost/?act=register),才能实行后面的代码。
2、利用预编译语句,避免sql注入
注入是早前很大的一个问题,不过近些年由于大家比较重视这个问题,以是逐步变得好了很多。
吴翰清在web白帽子里说的很好,实在很多漏洞,像sql注入或xss,都是将“数据”和“代码”没有区分开。“代码”是程序员写的内容,“数据”是用户可以改变的内容。如果我们写一个sql语句select from admin where username='admin' password='xxxxx', admin和xxxxx便是数据,是用户输入的用户名和密码,但如果没有任何处理,用户输入的就可能是“代码”,比如'or ''=',这样就造成了漏洞。“代码”是绝对不能让用户打仗的。
在php中,对付mysql数据库有两个模块,mysql和mysqli,mysqli的意思便是mysql improve。mysql的改进版,这个模块中就含有“预编译”这个观点。像上面那个sql语句,改一改:select from admin where username='?' password='?',它就不是一个sql语句了,但是可以通过mysqli的预编译功能先把他编译成stmt工具,在后期用户输入账号密码后,用stmt->bind_param将用户输入的“数据”绑定到这两个问号的位置。这样,用户输入的内容就只能是“数据”,而不可能变成“代码”。
这两个问号限定了“数据”的位置,以及sql语句的构造。我们可以把我们所有的数据库操作都封装到一个类中,所有sql语句的实行都进行预编译。这样就完备避免了sql注入,这也是吴翰清最推举的办理方案。
下面是利用mysqli的一些代码部分(所有的判断函数运行成功或失落败的代码我都省略了,但不代表不主要):
<?php
//用户输入的数据
$name = 'admin';
$pass = '123456';
//首先新建mysqli工具,布局函数参数中包含了数据库干系内容。
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT);
//设置sql语句默认编码
$this->mysqli->set_charset(\公众utf8\公众);
//创建一个利用通配符的sql语句
$sql = 'SELECT user_id FROM admin WHERE username=? AND password=?;';
//编译该语句,得到一个stmt工具.
$stmt = $conn->prepare($sql);
/之后的内容就能重复利用,不用再次编译/
//用bind_param方法绑天命据
//大家可以看出来,由于我留了两个?,也便是要向个中绑定两个数据,以是第一个参数是绑定的数据的类型(s=string,i=integer),第二个往后的参数是要绑定的数据
$stmt->bind_param('ss', $name, $pass);
//调用bind_param方法绑定结果(如果只是检讨该用户与密码是否存在,或只是一个DML语句的时候,不用绑定结果)
//这个结果便是我select到的字段,有几个就要绑定几个
$stmt->bind_result($user_id);
//实行该语句
$stmt->execute();
//得到结果
if($stmt->fetch()){
echo '上岸成功';
//一定要把稳开释结果资源,否则后面会出错
$stmt->free_result();
return $user_id; //返回刚才select到的内容
}else{echo '登录失落败';}
?>
3、预防XSS代码,如果不须要利用cookie就不该用
在我的网站中并没有利用cookie,更由于我对权限限定的很去世,以是对付xss来说危险性比较小。
对付xss的防御,也是一个道理,处理好“代码”和“数据”的关系。当然,这里的代码指的便是javascript代码或html代码。用户能掌握的内容,我们一定要利用htmlspecialchars等函数来处理用户输入的数据,并且在javascript中要谨慎把内容输出到页面中。
4、限定用户权限,预防CSRF
现在脚本漏洞比较火的便是越权行为,很多主要操作利用GET办法实行,或利用POST办法实行而没有核实实行者是否知情。
CSRF很多同学可能比较陌生,实在举一个小例子就行了:
A、B都是某论坛用户,该论坛许可用户“赞”某篇文章,用户点“赞”实在是访问了这个页面:http://localhost/?act=support&articleid=12。这个时候,B如果把这个URL发送给A,A在不知情的情形下打开了它,即是说给articleid=12的文章赞了一次。
以是该论坛换了种办法,通过POST办法来赞某篇文章。
<form action=\"大众http://localhost/?act=support\"大众 method=\"大众POST\公众>
<input type=\"大众hidden\公众 value=\"大众12\公众 name=\"大众articleid\公众>
<input type=\公众submit\"大众 value=\"大众赞\"大众>
</form>
可以看到一个隐蔽的input框里含有该文章的ID,这样就不能通过一个URL让A点击了。但是B可以做一个“极具诱惑力”的页面,个中某个按钮就写成这样一个表单,来诱惑A点击。A一点击,依旧还是赞了这篇文章。
末了,该论坛只好把表单中增加了一个验证码。只有A输入验证码才能点赞。这样,彻底去世了B的心。
但是,你见过哪个论坛点“赞”也要输入验证码?
以是吴翰清在白帽子里也推举了最好的办法,便是在表单中加入一个随机字符串token(由php天生,并保存在SESSION中),如果用户提交的这个随机字符串和SESSION中保存的字符串同等,才能赞。
在B不知道A的随机字符串时,就不能越权操作了。
我在网站中也多次利用了TOKEN,不管是GET办法还是POST办法,常日就能抵御99%的CSRF估计了。
5、严格掌握上传文件类型
上传漏洞是很致命的漏洞,只要存在任意文件上传漏洞,就能实行任意代码,拿到webshell。
我在上传这部分,写了一个php类,通过白名单验证,来掌握用户上传恶意文件。在客户端,我通过javascript先验证了用户选择的文件的类型,但这只是善意地提醒用户,终极验证部分,还是在做事端。
白名单是必要的,你如果只许可上传图片,就设置成array('jpg','gif','png','bmp'),当用户上传来文件后,取它的文件名的后缀,用in_array验证是否在白名单中。
在上传文件数组中,会有一个MIME类型,见告做事端上传的文件类型是什么,但是它是不可靠的,是可以被修正的。在很多存在上传漏洞的网站中,都是只验证了MIME类型,而没有取文件名的后缀验证,导致上传任意文件。
以是我们在类中完备可以忽略这个MIME类型,而只取文件名的后缀,如果在白名单中,才许可上传。
当然,做事器的解析漏洞也是很多上传漏洞的打破点,以是我们只管即便把上传的文件重命名,以“日期韶光+随机数+白名单中后缀”的办法对上传的文件进行重命名,避免由于解析漏洞而造成任意代码实行。
6、加密稠浊javascript代码,提高攻击门槛
很多xss漏洞,都是黑客通过阅读javascript代码创造的,如果我们能把所有javascript代码稠浊以及加密,让代码就算解密后也是混乱的(比如把所有变量名更换成其MD5 hash值),提高阅读的难度。
7、利用更高等的hash算法保存数据库中主要信息
这个硬盘容量大增的期间,很多人拥有很大的彩虹表,再加上类似于cmd5这样的网站的大行其道,纯挚的md5已经等同于无物,以是我们急迫的须要更高等的hash算法,来保存我们数据库中的密码。
所往后来涌现了加salt的md5,比如discuz的密码便是加了salt。实在salt便是一个密码的“附加值”,比如A的密码是123456,而我们设置的salt是abc,这样保存到数据库的可能便是md5('123456abc'),增加了破解的难度。
但是黑客只要得知了该用户的salt也能跑md5跑出来。由于现在的打算机的打算速率已经非常快了,一秒可以打算10亿次md5值,弱一点的密码分把钟就能跑出来。
所往后来密码学上改进了hash,引进了一个观点:密钥延伸。说大略点便是增加打算hash的难度(比如把密码用md5()函数循环打算1000次),故意减慢打算hash所用的韶光,以前一秒可以打算10亿次,改进后1秒只能打算100万次,速率慢了1000倍,这样,所需的韶光也就增加了1000倍。
那么对付我们,怎么利用一个安全的hash打算方法?大家可以翻阅emlog的源码,可以在include目录里面找到一个HashPaaword.php的文件,实在这便是个类,emlog用它来打算密码的hash。
这个类有一个特点,每次打算出的hash值都不一样,以是黑客不能通过彩虹表等办法破解密码,只能用这个类中一个checkpassword方法来返回用户输入密码的精确性。而该函数又特意增加了打算hash的韶光,以是黑客很难破解他们拿到的hash值。
在最新的php5.5中,这种hash算法成为了一个正式的函数,往后就能利用该函数来hash我们的密码了
8、验证码安全性
验证码常日是由php脚本天生的随机字符串,通过GD库的处理,制作成图片。真正的验证码字符串保存在SESSION中,然后把天生的图片展示给用户。用户填写了验证码提交后,在做事端上SESSION中的验证码进行比对。
由此想到了我之前犯过的一个缺点。验证码比对完成之后,不管是精确还是缺点,我都没有清理SESSION。这样产生了一个问题,一旦一个用户第一次提交验证码成功,第二次往后不再访问天生验证码的脚本,这时候SESSION中的验证码并没有更新,也没有删除,导致验证码重复利用,起不到验证的浸染。
再就说到了验证码被识别的问题,wordpress包括emlog的程序我常常会借鉴,但他们所利用的验证码我却不敢阿谀。很多垃圾评论都是验证码被机器识别后产生的,以是我后来也利用了一个繁芜一点的验证码,听说是w3c推举利用的。
好了,我能想到的,也是在实际利用中用到的东西也就这么多了。这也仅仅是我自己写代码中积累的一些对代码安全性的一个见地,如果大家还有更好的想法,可以和我互换。希望大家也能写出更安全的代码。