前端安全一直是一个蛮严苛的问题,特别如果设计到money更是如此。了解前端安全,在平时的coding中主动考虑,防范于未然,是一个有追求的程序猿应该做的。
未登录
我们从弱弱的基本开始,第一步当然是登录鉴权了,如果一个需要用户身份鉴权的应用系统没有登录过滤那简直是没法想像的,方案基本都是用户输入用户名密码、或是三方 openID 授权后在 session 里保存用户此次登录的凭证来确保每次请求的合法性。
由于 session有时效限制,所以若用户一段时间未与服务器交互则会过期重登,当然我们也可以通过把登录凭证存在 cookie 里来自由控制用户登录的有效时间。这个是最基本的鉴权我们就不深入细节。
登录了,但被CSRF
虽然有了登录验证后,我们可以挡掉其他非登录用户的骚扰了,但悲剧的是坏人们还是可以欺骗我们善良的用户,借已登录用户的手来搞破坏。即 CSRF(Cross-site request forgery)跨站请求伪造。
举个例子:
有个黑客的网站 h.com,我们的网站 a.com。用户登录了a.com,但被诱点进入h.com(如收到 QQ 消息或邮件传播的h.com 的链接),当用户访问这个链接时,h.com 上自动发送一个请求到 a.com,由于用户已登录a.com,浏览器根据同源策略,会在该请求上自动附带了 cookie,而前面我们提到了鉴权是通过 cookie 里的某个 key 值凭证的,所以如若没有判断该请求的来源合法性,我们则通过了该伪造的请求,执行了相应的操作。比如这个请求是让该用户发一篇日志,或是发微博,或是严重的发起一笔转账。
常见的诸如放一张看不见的图片发起get请求
<img src=http://www.a.com/Transfer.phptoUserId=999&money=1000000>
post 请求会稍微麻烦些,但同样很好实现,可以构造一个表诱导用户点击,也可以直接利用ajax发送post请求。
要防住此类伪造请求我们第一反应都是检查这个请求的来源,确实,在上述的情形下发来的请求报文里referer字段的网址不是我们的自己站点,
而会是一个三方的,如上假设的 h.com。但是很多情况下,referer并不完全靠谱,比如如果众多二级域名之间需要通信,那么referer可能会
设得比较泛,如*.a.com。或是历史原因一些 referer 为空的请求会漏过校验等。所以这种方式只是提高了破解成本,并不能完全杜绝。
现在业界比较通用的解决方案还是在每个请求上附带一个anti-CSRF token。
将sessionid加盐再散列处理。然后一起发送给后端。服务器端拿到 token 后用相同的算法对 sid 运算后匹对,相同则为已登录用户发出请求,没有或不对等则说明该请求是伪造的。
token = MD5( sid * salt )
其实这个算法的精髓在于使用了 cookie 中的 sid(用户登录后我们服务器种的 cookie 凭证),因为前端的代码对用户而言都是没有秘密的,只要花点时
间即可推算出我们的算法,但由于攻击者无法登录,又拿不到 cookie 里的 sid(根据浏览器的同源策略,在 h.com 上无法获取属于 a.com 的 cookie),所以无法构造出 token。
至于加 MD5当然是因为我们不会傻的把登录凭证 sid 放到 url 上给人直接拿了登录- -(以前还真有人干过),为什么要加 盐 salt 则是怕简单的一层 MD5还是有可能被通过撞库的方式解出 sid,当然加了 salt 也不意味着100%防住,只是大大提高了破解的成本而已。
有防 CSRF了,但被 XSS
从上面我们知道防住 CSRF 最关键的是要守住 cookie,如果用户的 cookie 被人窃取了,那上面的防护就形同虚设了。而 XSS 就可以很轻易的获取用户的 cookie,
所以有句话叫Buy one XSS, get a CSRF for free。
用户输入的内容原封不动的通过服务器程序渲染在页面上 。
反射型
举个栗子
前端get一个请求:
后台处理:
<?php echo 'Hello' . $_GET['name'];
代码本意是根据queryString 的 name 来动态展示用户名,但由于未对 name 做编码校验,当链接为:
www.a.com?xss.php?name=<script>alert(document.cookie);</script>
这时访问这个链接则会弹出我们的 cookie 内容,如果这时候再把 alert 改为一个发送函数,则可把 cookie 偷走。
前端DOM-Based XSS
<script>
document.getElementById('intro-div').innerHTML = document.location.href.substring(document.location.href.indexOf("intro=")+6);
</script>
如上,直接将用户的输出输出到页面标签中。但是如果将链接中的内容设置为
http://www.a.com/index.html?intro=<script>alert(document.cookie)</script>
那我们的 cookie 又没了。
持久型XSS
也称为存储型 XSS,注入脚本跟 XSS 大同小异,只是脚本不是通过浏览器->服务器->浏览器这样的反射方式,而是多发生在富文本编辑器、日志、留言、配置系统等数据库保存用户输入内容的业务场景。即用户的注入脚本保存到了数据库里,其他用户只要一访问到都会中招。
前端get一个请求:
www.a.com/xss.php?name=<script>alert(document.cookie);</script>
后台处理:
<?php $db.set("name", $_GET["name"]);
前端请求的页面:
<?php echo 'Hello' . $db.get['name'];
这样但凡请求了该页面的都会被XSS攻击到。
解决XSS
从上面我们可以看出各种攻击手段很重要的一点就是要获取 cookie,有了 cookie 就相当于获取了我们用户的身份信息,所以自然的我们要保护我们的 cookie。在 cookie 里有个 HttpOnly 属性可以让页面无法通过 JS 来读写 cookie。
res.cookie('a', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
开启这个属性后 document 将无法获取 cookie。当然这个方法也不是万能的,我们的 cookie 还是会在 header 中,还是有其他手段去获取 header 中的 cookie,
不过使用后我们还是提高了攻击的成本。关键还是我们要不相信用户的一切输入,对编码输出在页面中会破坏原有代码(HTML、JavaScript甚至WML等)规则的特殊字符以及对某些标签的某些属性进行白名单检查。
XSS防护也做了,被用户SQL注入
举个例子:
功能是查询userId为123的用户出来,这个请求到我们服务端最后sql语句是这样:
select * from users where userid=123
如果不做任何校验,如果用户输入如下
123; DROP TABLE users;
嘎嘎,整个表就没有了。
所以同样的,还是那个原则,我们不能相信用户的任何输入,如果一个sql语句里包含了用户输入的内容,那我们要对内容做sql安全相关的过滤检查。
同时,使用一些ORM工具,不使用拼凑字符串型的语句执行方式。
总结
总的来说,前端最重要的就是一个sessionId这个代表用户身份的凭证,保护好这个凭证,同时利用同源策略以及自己加密token来识别用户,最后以最恶意的眼光对待用户的输入,不要相信用户的输入。这样就能屏蔽绝大部分常见的安全问题了。