CSRF的全程是Cross Site Request Forgery, 即跨站请求伪造。深入了解CSRF后会发现,这种攻击非常危险,并且很容易就忽视掉。那到底什么是CSRF呢,我们通过一个案例来了解一下
有这么一个简单的后台页面,可以发表、删除以及编辑文章,点击删除的那个按钮,就可以把一篇文章删掉。因为偷懒,想说如果我把这个功能做成 GET,我就可以直接用一个链接完成删除这件事,在前端几乎不用写到任何代码:
<a href='/delete?id=3'>删除a>
很方便对吧?然后我在后端那边做一下验证,验证 删除的request的cookie 有没有带 session id 上来,也验证这篇文章是不是这个 id 的作者写的,都符合的话才删除文章。
嗯,听起来该做的都做了啊,我都已经做到:「只有作者本人可以删除自己的文章」了,应该很安全了,难道还有哪里漏掉了吗?
没错,的确是「只有作者本人可以删除自己的文章」,但如果他不是自己「主动删除」,而是在不知情的情况下删除呢?你可能会觉得我在讲什么东西,怎么会有这种事情发生,不是作者主动删的还能怎么删?
好,我就来让你看看还能怎么删!
今天假设小黑是一个邪恶的坏蛋,想要让小明在不知情的情况下就把自己的文章删掉,该怎么做呢?
小黑知道小明很喜欢心理测验,于是就做了一个心理测验网站,并且发给小明。但这个心理测验网站跟其他网站不同的点在于,「开始测验」的按钮代码长得像这样:
<a href='https://Livingblog.com/delete?id=3'>开始测试a>
是不是觉得开始不对劲了?
小明收到网页之后很开心,就点击「开始测验」。点击之后浏览器就会发送一个 GET 请求https://Livingblog.com/delete?id=3,并且因为浏览器的运行机制,一并把 Livingblog.com 的 cookie 都一起带上去。
Server 端收到之后检查了一下 session,发现是小明,而且这篇文章也真的是小明发的,于是就把这篇文章给删除了。
这就是CSRF, 你现在明明在心理测验网站,假设是 https://test.com 好了,但是却在不知情的状况下删除了 https://Livingblog.com 的文章
更有甚者,如果把get请求放在一张看不见的图片链接里:
<img src='https://Livingblog.com/delete?id=3' width='0' height='0' />
那更是神不知鬼不觉了
我们举的例子是删除文章,这你可能觉得没什么,那如果是银行转帐呢?如果你不幸负责了银行网站的代码编写,而你没有对CSRF攻击进行干预。攻击者只要在自己的网页上写下转帐给自己帐号的 code,再把这个网页散布出去就好,就可以收到一大堆钱。
我:我把请求方法换成post请求怎么样,这样不就无法透过 或是
来攻击了吗
CSRF:简单~,我用form表单元素
<form action="https://Livingblog.com/delete" method="POST">
<input type="hidden" name="id" value="3"/>
<input type="submit" value="开始测试"/>
form>
并且我开一个看不见的 iframe,让 form submit 之后的结果出现在 iframe 里面,而且这个 form 还可以自动 submit,完全不需要经过用户的任何操作
<iframe style="display:none" name="csrf-frame">iframe>
<form method='POST' action='https://small-min.blog.com/delete' target="csrf-frame" id="csrf-form">
<input type='hidden' name='id' value='3'>
<input type='submit' value='submit'>
form>
<script>document.getElementById("csrf-form").submit()script>
我:那后端改成只接收 json 呢?
CSRF:笑死,如果你的 api 的 Access-Control-Allow-Origin 设成 * 的话,代表任何 domain 都可以发送 ajax 到你的 api server,这样无论你是改成 json,或是把 method 改成 PUT, DELETE 都没有用。
我:妈的,那请求我都带上图像验证,或者是短信验证。
CSRF:好吧,那我确实没办法了,网页的重要操作也确实是这么干的,但如果使用者每次删除 blog 都要打一次图形验证码,他们应该会烦死吧。
我:可恶啊,CSRF不可小觑啊,我要认真思考了。要防止 CSRF 攻击,我们其实只要确保有些资讯「只有使用者知道」即可。那该怎么做呢?
我在 form 里面加上一个 hidden 的栏位,叫做csrftoken,这里面填的值由 server 随机产生,并且存在 server 的 session 中。
按下 submit 之后,server 比对表单中的csrftoken与自己 session 里面存的是不是一样的,是的话就代表这的确是由使用者本人发出的 request。这个 csrftoken 由 server 产生。
<form action="https://Livingblog.com/delete" method="POST">
<input type="hidden" name="id" value="3"/>
<input type="hidden" name="csrftoken" value="fj1iro2jro12ijoi1"/>
<input type="submit" value="刪除文章"/>
form>
因为攻击者并不知道 csrftoken 的值是什么,也猜不出来,所以自然就无法进行攻击了。
CSRF:可是有另外一种状况,如果你的 server 支持跨域的请求,会发生什么事呢?我就可以在你的页面发起一个 request,顺利拿到这个 csrftoken 并且进行攻击。不过前提是你的 server 接受这个 domain 的 request。
我:那我再想想,延续「有些资讯只有使用者知道即可」的思路,继续在 server 产生一组随机的 token 并且加在 form 上面。但不同的点在于,除了不用把这个值写在 session 以外,同时也让客户端设定一个名叫 csrftoken 的 cookie,就是在form表单里和cookie里同时放一个值是一样的csrftoken 。这样的话,由于浏览器的限制——攻击者没法获取到cookie,只是在发起请求时会携带。那攻击者自然也就无法在form表单里填上与cookie一致的csrftoken。我在server里一对比就能知道了是恶意提交了!
CSRF:看似完美,可惜还是不够,看这个:双提交cookie的脆弱 :double-submit-cookies-vulnerabilities。阅读cookie是很难,可我可以自己伪造一个cookie,没想到吧
我:(眉头紧皱),那浏览器呢? CSRF之所以能成,是因为浏览器的机制所导致的,有没有可能从浏览器方面下手,来解决这个问题呢?Google 在 Chrome 51 版的时候正式加入了这个功能:SameSite cookie。在设置cookie的时候加上了这个功能。那么只要是浏览器验证不是在同一个 site 底下发出的 request,全部都不会带上这个 cookie。
CSRF:试着用吧, 看这个:SameSite浏览器版本支援 ,似乎对老浏览器的支援度不够啊。
道高一尺魔高一丈,还有更多的开发者与CSRF的斗法场景,比如Amazon的双cookie策略。客户端的双提交cookie等等。在这起到一个抛砖引玉的作用,keep moving