浅谈php安全性需要注意的几点事项
这段时间一直在写一个整站,前几天才基本完成了,所以抽个时间写了一篇对于php安全的总结。技术含量不高,过不了也没关系,希望能一些准备写网站的朋友一点引导
在放假之初,我抽时间看了《白帽子讲web安全》,吴翰清基本上把web安全中所有能够遇到的问题、解决思路归纳总结得很清晰,也是我这一次整体代码安全性的基石。
我希望能分如下几个方面来分享自己的经验
把握整站的结构,避免泄露站点敏感目录
在写代码之初,我也是像很多老源码一样,在根目录下放上index.php、register.php、login.php,用户点击注册页面,就跳转到。并没有太多的结构的思想,像这样的代码结构,最大的问题倒不是安全性问题,而是代码扩展与移植问题。
在写代码的过程中,我们常要对代码进行修改,这时候如果代码没有统一的一个入口点,我们可能要改很多地方。后来我读了一点emlog的代码,发现网站真正的前端代码都在模板目录里,而根目录下就只有入口点文件和配置文件。这才顿悟,对整个网站的结构进行了修改。
网站根目录下放上一个入口点文件,让它来对整个网站所有页面进行管理,这个时候注册页面变成了,任何页面只是act的一个参数,在得到这个参数后,再用一个switch来选择要包含的文件内容。在这个入口点文件中,还可以包含一些常量的定义,比如网站的绝对路径、网站的地址、数据库用户密码。以后我们在脚本的编写中,尽量使用绝对路径而不要使用相对路径(否则脚本如果改变位置,代码也要变),而这个绝对路径就来自入口点文件中的定义。
当然,在安全性上,一个入口点文件也能隐藏后台地址。像这样的地址不会暴露后台绝对路径,甚至可以经常更改,不用改变太多代码。一个入口点文件也可以验证访问者的身份,比如一个网站后台,不是管理员就不允许查看任何页面。在入口点文件中就可以验证身份,如果没有登录,就输出404页面。
有了入口点文件,我就把所有非入口点文件前面加上了这句话:
WWW_ROOT是我在入口点中定义的一个常量,如果用户是通过这个页面的绝对路径访问(),我就输出404错误;只有通过入口点访问(),才能执行后面的代码。
使用预编译语句,避免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的一些代码部分(所有的判断函数运行成功或失败的代码我都省略了,但不代表不重要):
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 '登录失败';} ?>
预防XSS代码,如果不需要使用cookie就不使用
在我的网站中并没有使用cookie,更因为我对权限限制的很死,所以对于xss来说危险性比较小。
对于xss的防御,也是一个道理,处理好“代码”和“数据”的关系。当然,这里的代码指的就是javascript代码或html代码。用户能控制的内容,我们一定要使用htmlspecialchars等函数来处理用户输入的数据,并且在javascript中要谨慎把内容输出到页面中。
限制用户权限,预防CSRF
现在脚本漏洞比较火的就是越权行为,很多重要操作使用GET方式执行,或使用POST方式执行而没有核实执行者是否知情。
CSRF很多同学可能比较陌生,其实举一个小例子就行了:
A、B都是某论坛用户,该论坛允许用户“赞”某篇文章,用户点“赞”其实是访问了这个页面:。这个时候,B如果把这个URL发送给A,A在不知情的情况下打开了它,等于说给articleid=12的文章赞了一次。
所以该论坛换了种方式,通过POST方式来赞某篇文章。
可以看到一个隐藏的input框里含有该文章的ID,这样就不能通过一个URL让A点击了。但是B可以做一个“极具诱惑力”的页面,其中某个按钮就写成这样一个表单,来诱惑A点击。A一点击,依旧还是赞了这篇文章。
最后,该论坛只好把表单中增加了一个验证码。只有A输入验证码才能点赞。这样,彻底死了B的心。
但是,你见过哪个论坛点“赞”也要输入验证码?
所以吴翰清在白帽子里也推荐了最好的方式,就是在表单中加入一个随机字符串token(由php生成,并保存在SESSION中),如果用户提交的这个随机字符串和SESSION中保存的字符串一致,才能赞。
在B不知道A的随机字符串时,就不能越权操作了。
我在网站中也多次使用了TOKEN,不管是GET方式还是POST方式,通常就能抵御99%的CSRF估计了。
严格控制上传文件类型
上传漏洞是很致命的漏洞,只要存在任意文件上传漏洞,就能执行任意代码,拿到webshell。
我在上传这部分,写了一个php类,通过白名单验证,来控制用户上传恶意文件。在客户端,我通过javascript先验证了用户选择的文件的类型,但这只是善意地提醒用户,最终验证部分,还是在服务端。
白名单是必要的,你如果只允许上传图片,就设置成array('jpg','gif','png','bmp'),当用户上传来文件后,取它的文件名的后缀,用in_array验证是否在白名单中。
上一篇: 百度导航Android版问题集
下一篇: php验证码的制作思路和实现方法