CSRF攻击
CSRF(cross site request forgery,跨站请求伪造)是一种网络攻击,这种攻击方式在HTTP协议出现时即存在,令其名声大噪的事件,是在2007年,谷歌旗下的Gmail因为CSRF漏洞被黑客攻击而造成了巨大损失。先来了解一下CSRF攻击原理,如果读者能理解原理最好,如果不能理解,学完cookie和session部分的知识再回过头来理解也可以。
CSRF攻击原理:网站通常都是通过cookie来实现登录功能的,而浏览器在访问某个网站时,会自动把这个网站之前保存在浏览器中的cookie数据携带到服务器上去。这时候就存在一个漏洞,假设现在有一个病毒网站,这个网站在源代码中添加了恶意的JavaScript代码,在你访问这个网站时,会自动给具有CSRF漏洞的网站服务器发送请求(如给某个银行发起转账请求)。因为在发送请求时,浏览器会自动把这个网站的cookie数据发送给对应的服务器,而此时对应的服务器(如某银行网站)不知道这个请求是伪造的还是用户自己发起的,就被欺骗过去了,从而达到在用户不知情的情况下,给某个服务器发送了一个请求(如转账),从而造成了损失。
防御CSRF攻击原理
CSRF攻击的要点,就是在向服务器发送请求时,相应的cookie会自动地被发送给对应的服务器,而服务器不知道这个请求是用户发起的还是伪造的。因此,可以在用户每次访问有表单的网页时,在表单中加入一个随机的字符串,如csrf_token,同时在cookie中也加入一个具有相同值的csrf_token键-值对。以后再给服务器发送请求时,必须在表单以及cookie中都携带csrf_token。因为在不同域名下的JavaScript无法操作对方的cookie,所以服务器只有在检测到cookie中的csrf_token和表单中的csrf_token相同时,才认为这个请求是正常的,否则就认为请求是伪造的,服务器就会进行防御,那么黑客就没办法进行攻击了。
Flask-WTF防御CSRF攻击
使用Flask-WTF可以方便地实现CSRF防御。CSRF防御可以分成3种方式,第1种是全局防御,第2种是使用单表单防御,第3种是使用AJAX。这3种方式的前提都是已经在cookie中设置好了csrf,然后再从表单中获取csrf_token,对比两者是否一致,一致则验证通过,否则即为验证失败。下面分别进行讲解。
1)全局防御
CSRF全局防御是通过创建Flask-WTF中的CSRFProtect类对象实现的,这个类接收app作为参数,在使用CSRFProtect创建对象后,其会自动在Jinja2模板的上下文中添加csrf_token函数,然后通过在模板中调用这个函数,就会自动生成csrf_token。app.py中创建CSRFProtect对象的示例代码如下。
from flask_wtf import CSRFProtect
app = Flask(__name__)
app.secret_key = "自定义的app密钥"
CSRFProtect(app)
添加完以上代码后,我们就可以在模板中使用 csrf_token 函数来获取值了。如还是以 register.html 为例,添加完 csrf_token 后的 register.html 代码如下。
<form action="{{ url_for('register') }}" method="POST">
<table>
<tr>
<td></td>
<td><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"></td>
</tr>
<!-- 其他表单字段 -->
</table>
</form>
上述代码中,添加了一个type为hidden的input标签,这个标签不会被显示在浏览器上,但是单击“提交”按钮时,还是会把这个标签上的值发送给服务器。然后还给这个标签的name设置为csrf_token,除非通过配置文件已经修改了,否则必须将该input标签的name值设置为csrf_token,接着再调用csrf_token函数,设置value属性的值。这样渲染完模板后,在浏览器中访问http://127.0.0.1:5000/register,然后在页面右击,在弹出的快捷菜单中选择“查看网页源代码”命令,在源代码页面可以看到生成类似如图6-4所示的代码。

表单中有了csrf_token以后,在视图函数中调用form.validate()方法时,RegisterForm就会自动对比request.form中的csrf_token是否和cookie中的csrf值相等,相等就验证通过,否则就验证失败。
2)单表单防御
单表单防御是通过在模板中传递FlaskForm子类对象实现的,也就是6.2节中登录功能的实现。FlaskForm子类默认是开启了csrf防御,如果想要关闭,可以在创建表单对象时传递meta={"csrf":False}来实现,示例代码如下。
form = LoginForm(meta={"csrf":False})
如果要开启csrf防御,在创建表单时不传递meta参数即可。在login.html模板中,只要渲染form.csrf_token,就会自动生成type为hidden类型的input标签,示例代码如下。
<form action="" method="POST">
<table>
<tbody>
<tr>
<td></td>
<td>{{ form.csrf_token }}</td>
</tr>
<!-- 其他表单字段 -->
</tbody>
</table>
</form>
我们在浏览器中访问 http://127.0.0.1:5000/login ,然后查看其网页源代码,可以看到如图 6-5 所示代码。

同理,以后在视图函数中调用 form.validate() 方法时会自动和 cookie 中的 csrf 进行对比,判断是否验证通过。
3)使用AJAX
使用AJAX提交表单数据,前提是要开启全局CSRF保护。为了方便,首先我们会在HTML父模板的head标签内添加一个meta标签,然后把csrf_token渲染到meta标签中。渲染到父模板中的原因是,为了让所有子模板都能获取到csrf_token,示例代码如下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
<meta name="csrf_token" content="{{ csrf_token() }}">
</head>
<!-- 其他内容 -->
</html>
在使用JavaScript发送请求之前,首先从模板中获取csrf_token,然后再设置到请求头中。Flask-WTF验证csrf_token,除了用cookie和表单中的值对比外,还会用cookie和请求头中的值对比,从请求头中获取,是通过X-CSRFToken参数获取的。这里以jQuery为例,设置在每次发送请求之前都添加好csrf_token,示例代码如下。
var csrftoken = $('meta[name=csrf_token]').attr('content');
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
上述代码中,首先从模板文件中获取csrf_token的值,然后通过设置beforeSend方法,使得在每次发送请求之前都把csrf_token设置到请求头中,这样即可完成csrf的验证。