CSRF成因
GET型
脆弱代码
存在CSRF的GET型请求如下:
1 |
|
先使用http://127.0.0.1:8888/login?user=admin模拟用户admin登陆,从代码可以看到登陆后网站将用户身份(简单起见就是用户名)保存到session中。
接着访问http://127.0.0.1:8888/get可以看到用户具有这一权限,可以进行操作(假设这一链接是一个关键操作,如重置密码或泄露其他敏感信息的操作)。
攻击方法
如果用户访问了恶意网页,恶意网页诱导用户访问http://127.0.0.1:8888/get或是用js发送一个get请求,那么用户由于sessionID还存在于浏览器中,因此会在无意间使用自己的身份重置密码,如用于访问了如下内容的网页:
1 |
|
尽管同源策略导致js拿不到结果,但是请求仍然会正常发送:
POST 表单型
脆弱代码
有人认为CSRF产生的原因是因为GET请求类型造成的,其实不然,如下就是一个POST类型的代码,它同样存在CSRF问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def post():
if request.method == 'GET':
template="""
<form action="http://127.0.0.1:8888/reset" method="POST">
<input name="action" type="text">
<input type="submit">
</form>
"""
return render_template_string(template)
else:
print(request.form)
data=request.form["action"]
print("session:",session)
if session.get('user','')=='admin':
print("Admin do", data)
return "Admin do "+data
else:
print("No Privilege2...")
return "No Privilege2..."
攻击方法
攻击者只需要同样的伪造POST表单即可,这里我们换用fetch()
试一下,当然XMLHttpRequest()
也可以。
1 | fetch('http://127.0.0.1:8888/post',{method: 'POST', credentials: 'include', headers:{'Content-Type': 'application/x-www-form-urlencoded'}, body:'action=reset+password'}) |
尽管同源再一次没让我们拿到结果,但是请求还是发过去了:
POST JSON型
如果服务器严格限制了Content-Type=application/json
,理论上是不存在CSRF的,因为该请求属于非简单请求,非简单请求会发先检请求确认是否允许跨域,如果不允许跨域的话js就没法使用cookie。
但是若服务器不判断content-type,那么攻击者就可以使用表单或是js伪造一个Content-Type=text/plain
的请求。
脆弱代码
1 |
|
攻击方法
使用表单伪造post,关键是用name字段构造一个合法的json:
1
2
3
4<form action="http://127.0.0.1:8888/json" method="POST" enctype="text/plain">
<input name='{"action":"change passwd...", "test":"' value='test"}' type='hidden'>
<input type=submit>
</form>使用js伪造post json
1
fetch('http://127.0.0.1:8888/json',{method: 'POST', credentials: 'include', headers:{'Content-Type': 'text/plain'}, body:'{"action":"reset password"}'})
防御
主要有两种手段:
- 根据Referer判断,但是要考虑没有referer字段的情况
- 增加一个随机的Token,前端发送后,后端比较是否一致,本文主要介绍这一种方法
GET型
在产生GET请求的地方就加一个csrf_token,在处理get请求时判断csrf_token与cookie或session中的token是否一致。
POST表单型
对于flask,使用
1 | from flask_wtf.csrf import CSRFProtect |
打开csrf保护,接着再对所有的表单添加一个隐藏字段即可:
1 | <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> |
增加隐藏字段后,每次POST时都会带有一个csrf_token,攻击者由于同源策略是无法获取这个token的,另外token写进session里面,即session和token是一对一关系,因此攻击者也无法通过自己的token猜测别人的token,而服务器再POST请求过来时就会验证这个token是否与session一致,若不一致则拒绝服务,这样一来攻击者就无法攻击成功了(除了把token放表单里,还可以放cookie里,攻击者仍然无法获取)
POSTJSON型
理论上严格控制content-type: application/json
就能解决问题,当然也可以用通用方法:
同样还是先开启CSRF防御:
1 | from flask_wtf.csrf import CSRFProtect |
让自己站的ajax拿到csrf_token,就像之前说的,将token放进cookie里,使用app.after_request修饰使得每个页面返回时都执行:
1 |
|
接着ajax从cookie拿到token并放到headers里,不用担心攻击者,因为由于同源策略,他们没法获取其他网站的cookie:
1 |
|
参考链接
- CSRF-Scanner——打造全自动检测CSRF漏洞利器,https://security.tencent.com/index.php/blog/msg/24