CSRF
CSRF全拼为Cross Site Request Forgery,译为跨站请求伪造。CSRF指攻击者盗用了你的身份,以你的名义发送恶意请求。- 包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......
- 造成的问题:个人隐私泄露以及财产安全。
CSRF攻击示意图
- 客户端访问服务器时没有同服务器做安全验证

防止 CSRF 攻击
步骤
- 在客户端向后端请求界面数据的时候,后端会往响应中的 cookie 中设置 csrf_token 的值
- 在 Form 表单中添加一个隐藏的的字段,值也是 csrf_token
- 在用户点击提交的时候,会带上这两个值向后台发起请求
- 后端接受到请求,以会以下几件事件:
- 从 cookie中取出 csrf_token
- 从 表单数据中取出来隐藏的 csrf_token 的值
- 进行对比
- 如果比较之后两值一样,那么代表是正常的请求,如果没取到或者比较不一样,代表不是正常的请求,不执行下一步操作
代码演示
未进行 csrf 校验的 WebA
-
后端代码实现
1from flask import Flask, render_template, make_response 2from flask import redirect 3from flask import request 4from flask import url_for 5 6app = Flask(__name__) 7 8 9@app.route('/', methods=["POST", "GET"]) 10def index(): 11 if request.method == "POST": 12 # 取到表单中提交上来的参数 13 username = request.form.get("username") 14 password = request.form.get("password") 15 16 if not all([username, password]): 17 print('参数错误') 18 else: 19 print(username, password) 20 if username == 'laowang' and password == '1234': 21 # 状态保持,设置用户名到cookie中表示登录成功 22 response = redirect(url_for('transfer')) 23 response.set_cookie('username', username) 24 return response 25 else: 26 print('密码错误') 27 28 return render_template('temp_login.html') 29 30 31@app.route('/transfer', methods=["POST", "GET"]) 32def transfer(): 33 # 从cookie中取到用户名 34 username = request.cookies.get('username', None) 35 # 如果没有取到,代表没有登录 36 if not username: 37 return redirect(url_for('index')) 38 39 if request.method == "POST": 40 to_account = request.form.get("to_account") 41 money = request.form.get("money") 42 print('假装执行转操作,将当前登录用户的钱转账到指定账户') 43 return '转账 %s 元到 %s 成功' % (money, to_account) 44 45 # 渲染转换页面 46 response = make_response(render_template('temp_transfer.html')) 47 return response 48 49if __name__ == '__main__': 50 app.run(debug=True, port=9000) -
前端登录页面代码
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <title>登录</title> 6</head> 7<body> 8 9<h1>我是网站A,登录页面</h1> 10 11<form method="post"> 12 <label>用户名:</label><input type="text" name="username" placeholder="请输入用户名"><br/> 13 <label>密码:</label><input type="password" name="password" placeholder="请输入密码"><br/> 14 <input type="submit" value="登录"> 15</form> 16 17</body> 18</html> -
前端转账页面代码
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <title>转账</title> 6</head> 7<body> 8<h1>我是网站A,转账页面</h1> 9 10<form method="post"> 11 <label>账户:</label><input type="text" name="to_account" placeholder="请输入要转账的账户"><br/> 12 <label>金额:</label><input type="number" name="money" placeholder="请输入转账金额"><br/> 13 <input type="submit" value="转账"> 14</form> 15 16</body> 17</html>运行测试,如果在未登录的情况下,不能直接进入转账页面,测试转账是成功的
攻击网站B的代码
-
后端代码实现
1from flask import Flask 2from flask import render_template 3 4app = Flask(__name__) 5 6@app.route('/') 7def index(): 8 return render_template('temp_index.html') 9 10if __name__ == '__main__': 11 app.run(debug=True, port=8000) -
前端代码实现
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6</head> 7<body> 8 9<h1>我是网站B</h1> 10 11<form method="post" action="http://127.0.0.1:9000/transfer"> 12 <input type="hidden" name="to_account" value="999999"> 13 <input type="hidden" name="money" value="190000" hidden> 14 <input type="submit" value="点击领取优惠券"> 15</form> 16 17</body> 18</html>运行测试,在用户登录网站A的情况下,点击网站B的按钮,可以实现伪造访问
在网站A中模拟实现 csrf_token 校验的流程
-
添加生成 csrf_token 的函数
1# 生成 csrf_token 函数 2def generate_csrf(): 3 return bytes.decode(base64.b64encode(os.urandom(48))) -
在渲染转账页面的,做以下几件事情:
-
生成 csrf_token 的值
-
在返回转账页面的响应里面设置 csrf_token 到 cookie 中
-
将 csrf_token 保存到表单的隐藏字段中
1@app.route('/transfer', methods=["POST", "GET"]) 2def transfer(): 3 ... 4 # 生成 csrf_token 的值 5 csrf_token = generate_csrf() 6 7 # 渲染转换页面,传入 csrf_token 到模板中 8 response = make_response(render_template('temp_transfer.html', csrf_token=csrf_token)) 9 # 设置csrf_token到cookie中,用于提交校验 10 response.set_cookie('csrf_token', csrf_token) 11 return response -
在转账模板表单中添加 csrf_token 隐藏字段
1<form method="post"> 2 <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> 3 <label>账户:</label><input type="text" name="to_account" placeholder="请输入要转账的账户"><br/> 4 <label>金额:</label><input type="number" name="money" placeholder="请输入转账金额"><br/> 5 <input type="submit" value="转账"> 6</form> -
运行测试,进入到转账页面之后,查看 cookie 和 html 源代码

-
在执行转账逻辑之前进行 csrf_token 的校验
1if request.method == "POST": 2 to_account = request.form.get("to_account") 3 money = request.form.get("money") 4 # 取出表单中的 csrf_token 5 form_csrf_token = request.form.get("csrf_token") 6 # 取出 cookie 中的 csrf_token 7 cookie_csrf_token = request.cookies.get("csrf_token") 8 # 进行对比 9 if cookie_csrf_token != form_csrf_token: 10 return 'token校验失败,可能是非法操作' 11 print('假装执行转操作,将当前登录用户的钱转账到指定账户') 12 return '转账 %s 元到 %s 成功' % (money, to_account)
运行测试,用户直接在网站 A 操作没有问题,再去网站B进行操作,发现转账不成功,因为网站 B 获取不到表单中的 csrf_token 的隐藏字段,而且浏览器有同源策略,网站B是获取不到网站A的 cookie 的,所以就解决了跨站请求伪造的问题
在 Flask 项目中解决 CSRF 攻击
在 Flask 中, Flask-wtf 扩展有一套完善的 csrf 防护体系,对于我们开发者来说,使用起来非常简单
在 FlaskForm 中实现校验
-
设置应用程序的 secret_key
-
用于加密生成的 csrf_token 的值
app.secret_key = "#此处可以写随机字符串#"
-
-
在模板的表单中添加以下代码
<form method="post"> {{ form.csrf_token() }} {{ form.username.label }} {{ form.username }}<br/> {{ form.password.label }} {{ form.password }}<br/> {{ form.password2.label }} {{ form.password2 }}<br/> {{ form.submit }} </form> -
渲染出来的前端页面为:

设置完毕,cookie 中的 csrf_token 不需要我们关心,会自动帮我们设置
单独使用
-
设置应用程序的 secret_key
-
用于加密生成的 csrf_token 的值
app.secret_key = "#此处可以写随机字符串#"
-
-
导入 flask_wtf.csrf 中的 CSRFProtect 类,进行初始化,并在初始化的时候关联 app
1from flask.ext.wtf import CSRFProtect 2CSRFProtect(app) -
如果模板中有表单,不需要做任何事。与之前一样:
1<form method="post"> 2 {{ form.csrf_token }} 3 ... 4</form> -
但如果模板中没有表单,你仍需要 CSRF 令牌:
1<form method="post" action="/"> 2 <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> 3</form>