Django 框架CSRF 防御实现机制浅析
2021-04-24
(国防大学教研保障中心 北京 100091)
CSRF 攻击是一种常见的Web 攻击方式,随着Web 安全技术的发展,各类Web 后端框架,如Laravel、Express、Spring Boot、Django等,都内置了CSRF 的防御功能。本文以Django 框架为例,分析其CSRF 防御实现机制。
1 CSRF 攻击
CSRF(Cross-site request forgery,跨站请求伪造)是指攻击者伪装受信用户的请求,在受信网站上执行被攻击者非本意操作的攻击方法。攻击者使用CSRF 可以利用你的名义发送邮件消息、盗取账号、购买商品、虚拟货币转账等恶意操作,造成个人隐私泄露以及财产损失。
1.1 CSRF 攻击原理
http 协议是无状态的,如一个Web 客户连续获取一个需要认证访问的Web 服务器上的信息,可能需要反复进行认证。为了解决这个问题,人们设计了一种得到广泛应用的Cookie 机制。Cookie 一般由服务器生成,发送给浏览器,浏览器会将Cookie 的值保存到文本文件中,下次请求同一网站时就发送该Cookie 给服务器,从而实现保存客户与服务器之间的状态信息。
Cookie 最典型的应用就是判定注册用户是否已经登录,CSRF 就是通过利用已登录用户的Cookie 状态信息,伪装成受信任用户的请求而实现攻击的。例如用户A 登录了受信任站点B,用户A 登录信息验证通过以后,站点B 会在返回给用户A 浏览器的信息中带上已登录的Cookie 信息;用户A 在未清除登录站点B 的Cookie 或Cookie未到期情形下,访问恶意站点C;恶意站点C 的某个页面在返回给用户A 的数据中带有向站点B 发起的恶意请求,此时用户A 会自动向B 站点发出请求;站点B 根据用户A 请求所带的Cookie,判断此请求为受信用户A 所发送的。至此,恶意站点C 就达到了伪造用户A 请求的目的。
1.2 CSRF 的防御手段
(1)验证HTTP Referer
根据HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。因此,要防御 CSRF 攻击,网站可以验证每一个访问安全受限页面请求的 Referer 值,如果是Referer 值的域名与本网站域名相同或在允许的域名之内,则说明该请求是来自本网站的请求,是合法的,反之,则有可能是CSRF 攻击,拒绝该请求。
(2)增加随机Token
要抵御 CSRF 攻击,关键在于在请求中放入第三方所不能伪造的信息,且该信息不存在于 Cookie 中,那么可以在 HTTP 请求加入一个随机产生的 Token,并在服务器端建立一个拦截器来验证这个Token,以此判定这个请求是否伪造。Token 的值必须是随机的,可以放入HTTP 请求参数中或HTTP 请求头中。
(3)加入验证码
在转账、删除、授权等一些敏感操作的请求中,加入支付密码或者校验码等各类人工验证方法,能很好地遏制CSRF 攻击。
2 Django 框架CSRF 防御机制分析
Django 框架是一个基于Python 的开源Web 开发框架,它对常用Web 开发模式进行了高度封装。Django 框架在安全方面,为了解决Cookie 存储数据不安全的问题的,Django 框架引入session,用户浏览器只需在Cookie 存储一个sessionid,具体的数据则通过加密默认保存在服务端session 中,同时也集成了对XXS、SQL 注入、CSRF等Web 常见攻击的防范组件。为实现CSRF 的防御功能,Django 主要是通过django.middleware.csrf.CsrfViewMiddleware 中间件来实现。
2.1 Django 框架中间件
Django 中间件是一个轻量的、框架级别的插件系统,用来处理Django 的请求和响应,在全局范围内改变Django 的输入和输出[1]。中间件本质上就是一个自定义类,负责实现一些特定的功能,类中定义了固定的五个方法:process_request、process_view、process_exception、process_template_responseproces、process_response,Django 会在浏览器请求的特定过程中执行这些方法。具体执行过程如图1[2]。
2.2 Django 框架CSRF 功能设置
Django 框架中设置CSRF 功能可分为全局和局部。全局设置通过在项目配置文件settings.py 的MIDDLEWARE 参数中添加删除中间件django.middleware.csrf.CsrfViewMiddleware 来开启或者关闭CSRF 保护。局部设置通过@csrf_protect、@csrf_exempt 内置装饰器为视图函数强制设置或关闭CSRF 功能,即便settings.py 中没有设置全局中间件。同时还需要在前端模板的 Form 表单中加入{%csrf_token %}标签,如果通过AJAX 进行提交数据,则把csrftoken 放入请求头中提交。
2.3 Django 框架CSRF 中间件分析
从源码分析,CsrfViewMiddleware 中间件下共定义了7 个方法,其中定义了中间件的process_request、process_view、process_response三个固有方法以及_accept、_reject、_get_token、_set_token 四个私有方法。下面简要分析下三个固有方法的主要作用。
图1 具体执行过程
(1)process_request 方法
process_request 方法在Django 接收到http 请求之后,URL 匹配之前调用。通过_get_token 方法,从request 对象的Cookie 或session中获取csrf_token;如果获取的csrf_token 不为空时,将其赋值给request.META["CSRF_COOKIE"]。
(2)process_view 方法
process_view 方法在Django 执行URL 匹配之后,执行视图函数之前调用。首先判读视图函数是否被装饰器csrf_exempt 装饰,如果是则直接执行视图渲染模板。接着判断request 请求方法是否是GET、HEAD、OPTIONS、TRAC,如果不是以上方法,把request.META["CSRF_COOKIE"]赋值给csrf_token,如果csrf_token为None,则执行_reject 方法返回403 错误页面;如果csrf_token 不None,且为POST 方法时,则从Form 表单中的csrfmiddlewaretoken字段或 request.META 中的 X-CSRFToken 字段取值并赋值给request_csrf_token。最后通过特定算法对 csrf_token 和request_csrf_token 这两个值对比,相等就执行视图渲染模板,反之直接返回403 错误页面。
(3)process_response 方法
process_response 方法在所有响应返回给浏览器之前调用。通过_set_token 方法设置 Cookie 或 session 的 csrf_token 值为request.META[“CSRF_COOKIE”],返回response,客户端浏览器完成渲染。
2.4 模板渲染阶段csrf_token 的生成
从上面CsrfViewMiddleware 中间件功能分析来看,没有发现csrf_token 如何初始生成的,其实初始token 是在视图渲染阶段,通过django.middleware.csrf 模块的get_token(与私有_get_token 方法不同)函数生成的。
在process_request、process_view 方法正常执行完之后,就会执行视图函数或者视图类渲染模板。如果在模板中添加了{%csrf_token%}标签,那么模板在render 函数渲染过程中,会在context 字典参数中加入context['csrf_input']和context['csrf_token'],返回浏览器时会把{%csrf_token%}替换成Form 表单元素:,其中csrfmiddlewaretoken 值通过get_token 函数生成。如果表单中没有{%csrf_token%},渲染的时候context['csrf_token']不会被填充,从而不触发get_token 方法,也就不会产生Cookie 信息。
下面分析 get_token 函数生成 token 的过程:如果request.META["CSRF_COOKIE"]存在,则取其值,使用_unsalt_cipher_token 函数解析出32 个字符的csrf_secret 密钥;如果request.META["CSRF_COOKIE"]不存在,则使用_get_new_csrf_string函数重新生成一个 32 个字符的 csrf_secret 密钥,再调用_salt_cipher_secret 生成一个 64 个字符的字符串赋给request.META[“CSRF_COOKIE”]。两种情况判断得到的csrf_secret 密钥,最后通过_salt_cipher_secret 函数加密返回。需要注意的是_salt_cipher_secret(csrf_secret)每次的返回值都不一样,但csrf_secret值可以保持不变,也就是说每次刷新页面时,表单csrfmiddlewaretoken 值是随机的,浏览器Cookie 值保持不变,可见csrf_secret==_unsalt_cipher_token(_salt_cipher_secret(csrf_secret))。
2.5 Django 框架CSRF 防御过程分析
为更清晰的分析CRSF 防御过程,不考虑CsrfViewMiddleware 中间件与Django 其他中间件之间的执行流程,只分析该中间件的运行机制。下面我们以用户提交表单为例进行过程分析。
(1)Django 用户正常访问过程分析
Django 开启了CRSF 防御,页面表单嵌入{%csrf_token%},当用户首次打开表单页面(没有该网站的Cookie 数据),向服务器发出GET 请求,Django 在接收到请求之后,第一步分发到CsrfViewMiddleware 中间件,执行process_request 方法,此时获取Cookie 信息为空,则进行URL 匹配;第二步URL 匹配后执行process_view 方法,因请求方法是GET,则直接执行视图函数;第三步视图函数根据GET 请求方法,执行相应的render 函数渲染模板,系统检查到表单嵌入{%csrf_token%},且request.META[“CSRF_COOKIE”]为空,则执行get_token 函数,生成一个新的csrf_secret,csrf_secret 利用_salt_cipher_secret 函数加密返回,同时把该值赋值给request.META[“CSRF_COOKIE”]和新生成的csrfmiddlewaretoken input 标签。第四步模板渲染完之后会触发process_response,通过_set_token方法把request.META[“CSRF_COOKIE”]等一些其他Cookie 信息存入用户Cookie 中,把渲染后的模板页面返回给用户浏览器。这时用户浏览器中就显示出表单页面,同时表单中含有csrfmiddlewaretoken input标签,Cookie 中包含csrf_token 数据。至此一次完整的用户GET 请求结束。
用户填入表单数据,向浏览器发出POST 请求。第一步执行pro cess_request 方法,通过_get_token 方法获取Cookie 信息赋值给requ est.META[“CSRF_COOKIE”],经URL 匹配后,第二步执行process_view 方法,因请求为POST,且request.META["CSRF_COOKIE"]不为空,则分别从Cookie(request.META["CSRF_COOKIE"])和表单中csrfmiddlewaretoken 字段中取值,进行算法比较结果相同,则进入第三步执行视图函数,判断请求方法为POST,执行相应的处理。第四步把request.META[“CSRF_COOKIE”]值等一些其他Cookie 信息再次存入用户Cookie 中(Cookie 的值不变,但expire 信息会有变化),把渲染后的模板页面返回给用户浏览器。至此用户POST 请求过程执行完毕。
(2)Django 防御CSRF 攻击过程分析
攻击者在恶意网站中设置相同的表单字段,利用已登录用户的Cookie 状态信息(此时Cookie 中有数据),向授信网站发出POST 请求,当请求执行CSRF 中间件的process_view 方法时,因攻击请求的表单字段中不包含csrfmiddlewaretoken 字段,则直接返回403 错误页面。
Django 设计CSRF 防御方法中,csrfmiddlewaretoken 字段的值是随机变化的,且采用了加密算法,攻击者很难模拟csrfmiddlewaretoken 的值。为了用户更加安全,防止Cookie 信息泄露,Django 的Cookie 信息加密放入session 中。
3 结束语
目前主流Web 框架都已加入CSRF 防御功能,且配置过程简单,但大多数人对框架CSRF 防御机制了解不多,通过本文对Django 框架的CSRF 防御原理和实现机制的分析,希望给大家对CSRF 攻击的原理和Django 框架CSRF 防御架构设计提供一些参考,同时进一步增强大家的Web 安全防范意识。