写在前面的话
在本系列文章的上集中,我们跟大家介绍了关于CSRF的一些基本概念,并对常见的几种CSRF漏洞类型进行了讲解。那么接下来,我们就要跟大家讨论一下如何才能消灭CSRF。
现代保护机制
实际上,通过修改应用程序源代码来实现CSRF保护在很多情况下是不现实的,要么就是源代码无法获取,要么就是修改应用程序的风险太高了。但我们所设计的解决方案可以轻松地部署到RASP、WAF、反向代理或均衡负载器中,并且可以同时保护一个或多个配置相同的应用程序。
首先我们要知道,正确地使用安全或不安全的HTTP verb是非常重要的。虽然这一点并不能构成一个有效的解决方案,但它是另外两种方法实现的基础。在构建应用程序之前,我们需要对其进行架构设计。幸运的是,大多数现代Web框架都有路由的概念,并且可以强制让节点与HTTP verb配对。在现代框架中,带有错误verb的请求将会导致错误的产生。如果你的应用程序中不能实现这种机制的话,请继续往下看。
另一种方法是验证请求的发送源,这种方法可以确保发送给应用程序的请求来自于一个受信任的源。在这里,正确使用HTTP verb同样是非常重要的,如果我们假设只有改变状态的请求会来自于不安全的请求,那我们就只需要对不安全的请求源进行验证就可以了。但正如我们之前所讨论的,在验证源的可靠性时我们还会遇到很多的问题。其中的一种解决方案是创建一个安全URL白名单,这样就可以防止来自外部源的CSRF。
第三种方法,也是最常见的方法,即使用令牌Token。令牌本身有多种形式,但大多数使用的都是同步器令牌(synchronizer token)。说得更加详细一点,这种令牌主要分为“双提交令牌”以及“加密令牌”。事实证明,结合使用双提交令牌以及加密令牌可以提供最好的安全性。
简单说来,所谓的同步器令牌,就是服务器和浏览器之间需要同步一个令牌(唯一的)。安全的请求方法会返回一个令牌,当浏览器在发送请求时会携带这个令牌,而服务器在处理请求之前,会验证令牌的有效性。处理完请求之后,服务器还会提供一个新的令牌以保证之前的令牌无法继续使用(防止重放攻击)。此时,攻击者将无法访问到令牌或者将其插入到恶意请求之中,因为如果攻击者想这样做的话,他必须要强迫目标用户向远程网站发送请求并访问请求内容,但SOP可以防止这种情况的发生。这样一来,攻击者所能使用的最后一种方法就是利用目标程序可能存在的XSS漏洞了。
需要注意的是,令牌主要有四个部分(一个随机数,用户识别符,过期时间以及真实性验证信息)组成,因此保持其“整体完整性”就非常重要了,其中缺少任何一项都将导致令牌的安全性大打折扣。
在令牌机制的实现过程中,有两个方面我们需要仔细斟酌,即服务器端和客户端。其中,服务器端负责生成和验证令牌,而客户端负责向需要请求资源的服务器发送令牌。需要注意的是,大家绝对有必要为每一个请求生成一个新的令牌,即使这样会牺牲一定的性能。除此之外,你也可以在cookie中添加令牌,但你需要确保cookie没有使用HttpOnly标记。下面这段简单的示例代码是生成令牌的常用方法:
- String generateToken(int userId, int key) {
- byte[16] data = random()
- expires = time() + 3600
- raw = hex(data) + "-" + userId + "-" + expires
- signature = hmac(sha256, raw, key)
- return raw + "-" + signature
- }
大家可以从上面这段代码中看到组成令牌的那四个部分。其中,HMAC是用于验证前三个元素有效性的令牌,并最终会添加到raw的结尾。
- bool validateToken(token, user) {
- parts = token.split("-")
- str = parts[0] + "-" + parts[1] + "-" + parts[2]
- generated = hmac(sha256, str, key)
- if !constantCompare(generated, parts[3]) {
- return false
- }
- if parts[2] < time() {
- return false
- }
- if parts[1] != user {
- return false
- }
- return true
- }
上面这段示例代码演示的是验证和计算令牌有效性的常用方法。首先我们需要将令牌拆分成它的四个组成部分,然后第一步就是利用前三个部分生成并验证HMAC的有效性(与之前的HMAC进行对比)。对比时间一定要确保使用的是固定时间,这样可以避免基于时间的攻击。如果验证成功,我们接下来就要确保令牌没有过期,最后进行用户匹配。但在真实场景中,最麻烦的事情就是让用户的浏览器在发送所有请求时自动提交令牌。
实际上在开发应用的过程中,绝大多数的现代框架都已经帮我们搞定这一切了。框架库可以处理XHR,并将令牌自动插入到请求信息(包括表单)中。但是如果框架没有帮我们实现的话,我们也可以自己实现这种功能。这一步主要可以分为两个部分,一个是处理表单提交,另一个是处理XHR。下面这段示例代码可以处理onclick事件回调:
- var target = evt.target;
- while (target !== null) {
- if (target.nodeName === 'A' || target.nodeName ===
- 'INPUT' || target.nodeName === 'BUTTON') {
- break;
- }
- targettarget = target.parentNode;
- }
- // We didn't find any of the delegates, bail out
- if (target === null) {
- return;
- }
我们可以将这段代码添加到文档中,而不是添加到单独的表单或可点击的元素之中,因为很有可能表单或元素根本就不存在与页面DOM之中。我们所指的元素是用户可以点击的东西,由于DOM树的结构以及事件处理系统的不同,所以我们要寻找的是那种可以提交表单的元素,例如input或button标签。
接下来,我们可以检测一个标签是否为input标签。如果它是,那么我们就可以确保这里有一个提交按钮了。当我们验证提交事件已经被触发之后,我们就可以继续搜索DOM树并寻找form标签了。如果找遍了DOM树却没有找到form标签,那么就说明元素没有被提交,除非它使用了XHR。找到form标签之后,最后一步就是将令牌以一个隐藏input元素添加到表单之中,即创建一个新的元素并将其添加到表单。
- var token =
- form.querySelector('input[name="csrf_token"]');
- var tokenValue = getCookieValue('CSRF-TOKEN');
- if (token !== undefined && token !== null) {
- if (token.value !== tokenValue) {
- token.value = tokenValue;
- }
- return;
- }
- var newToken = document.createElement('input');
- newToken.setAttribute('type', 'hidden');
- newToken.setAttribute('name', 'csrf_token');
- newToken.setAttribute('value', tokenValue);
- form.appendChild(newToken);
对于那些并非基于表单的请求,我们就需要想办法将令牌插入到XHR请求之中了。大多数代码库都提供了相关的抽象方法,包括jQuery,但我们需要针对标准XHR API创建我们自己的函数钩子。通过利用JavaScript的原型继承机制以及动态特性,我们可以直接将原始的发送方法添加到对象之中,这样我们就可以随时调用这些方法了。接下来,我们需要创建一个新的函数并将令牌插入到cookie中,然后再在请求信息中添加一个带值的header。
不过需要注意的是,对于IE浏览器,我们所设计的这种方法只适用于IE 8及其以上版本的IE浏览器,因为这些版本才支持方法原型和XHR,虽然IE 支持XHR但并不支持方法原型。具体的浏览器支持情况如下图所示:
总结
在本系列文章中,我们跟大家介绍了关于CSRF的一些基本概念,并对常见的几种CSRF漏洞类型进行了讲解。除此之外,我们还给大家提供了一些用于对付CSRF漏洞的最佳实践方法。这里我给大家推荐一款名叫Same-Site的扩展插件,它可以帮助我们对cookie进行检测,并对浏览器所发送的cookie进行严格的安全限制。这款插件的浏览器支持情况如下图所示: