Wiping Out CSRF
Posted 二向箔安全
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Wiping Out CSRF相关的知识,希望对你有一定的参考价值。
现在是2017年了,关于跨站请求伪造(CSRF)已经没有多少可以说的了。这是一个被熟知了多年的漏洞,在现在流行的 web 框架上也得到了一定的解决。那么为什么我们还在谈论呢?有几个原因:
传统的应用程序缺乏 CSRF 保护机制;
一些框架的构建不够完善;
应用程序没有很好的利用具有保护机制的框架;
新的应用程序不使用提供 CSRF 保护机制的现代框架。
CSRF 仍然是 Web 应用程序中普遍漏洞。 这篇文章将深入地分析 CSRF 的工作原理以及当下的一些防御对策。随后我们将提供一种解决对策,可以运用在应用程序写完之后,不需要修改源代码。最后,我们将检测 cookie 的一种新扩展,如果成为一种标准,这可能就是大多CSRF 案例的一个终结了。代码附在这里,其中包含测试用例的具体实现。GitHub存储库
理解攻击
在最根本的层面上,CSRF 是一个漏洞,攻击者强制受害者代表攻击者发出 HTTP 请求。这是一种完全发生在客户端的攻击(例如 Web 浏览器),其中接收方相信受害者正在发送可信任的应用程序信息。
有三个组件能够发生攻击:不正确的使用危险的HTTP请求方式、Web浏览器对cookie的处理和跨站脚本攻击(XSS)。
HTTP 规范标准将请求方式分为安全的和不安全的两种。安全的请求方式(GET,HEAD 和 OPTIONS)旨在用于只读操作。使用它们的请求旨在返回有关所请求的资源的信息,并且不会对服务器产生任何副作用。不安全请求方式(POST,PUT,PATCH 和 DELETE)用于修改,创建或删除资源。
不幸的是,HTTP 请求的的方式可能被忽略,或者是请求的意图没有被严格的限制。 不正确的请求方式使用的主要原因是由于 HTTP 规范的浏览器支持率历来较差。直到 XML HTTP 请求(XHR)的流行,除了 GET 或 POST 之外,根本不可能使用不依赖于特定框架和库黑客的请求方式。这种限制导致区分 HTTP 请求方式变的实际无关紧要。仅仅这样做不足以创造 CSRF 的条件,但它有助于并使其保护更加困难。
导致 CSRF 漏洞的最大因素是浏览器处理 Cookie 。 HTTP 最初被设计为无状态协议,具有对应于单个响应的单个请求,并且在请求之间不携带状态。为了支持复杂的Web 应用程序,创建 Cookie 作为在相关 HTTP 请求之间保持状态的解决方案。
Cookie 驻留在浏览器的全局级别,并在实例,窗口和标签之间共享。用户依赖于网页浏览器,可以自动传送每个请求的 Cookie 。由于 cookie 可以在浏览器中访问/修改,并且没有防篡改保护,所以状态的存储已经转移到服务器管理的 session 中。
在该模型中,在服务器上生成唯一的标识符并将其存储在 cookie 中。每个浏览器请求都会发送 cookie ,并且服务器能够查找该标识符,以确定它是否是一个有效的会话。会话结束时,服务器就不记得这个标识符了,之前发送的请求也将会失效。
问题在于浏览器是如何管理 cookie 的。一个 cookie 由几个属性组成,但我们关心的最重要的一个是 Domain 属性。Domain 属性的预期功能是将 Cookie 定位到与 cookie的 domain 属性相匹配的特定主机。这被设计为一种安全机制,以避免敏感信息(如会话标识符)被攻击者可能执行会话固定攻击的恶意网站被盗。这里的缺点是 domain 属性不依赖于同源策略 (SOP ),它只是将 Cookie 的 domain 的值与请求中的主机进行比较。
这允许源自不同来源的请求也携带该主机的任何 cookie 。当且仅当安全和不安全的请求方式被正确使用时,这是安全的行为; 示例:安全请求(GET)不应该改变状态,但是我们已经看到正确的使用不一定值得信任。如果您不了解 SOP,那么您应该阅读更多信息。
最重要的组件是跨站脚本攻击(XSS)。XSS 是受害者在 DOM 中呈现攻击者控制javascript 或 html 的能力。如果 XSS 存在于应用程序中,那么在阻止 CSRF 攻击时它也就结束了。如果 XSS 是有效的,我们将在本文中讨论的主要对策,大多数应用程序依赖,可以被绕过的。
进行攻击
现在我们知道这些因素,让我们深入了解 CSRF 的工作原理。如果您尚未设置,现在将是遵循代码库中的说明并获取示例运行的好时机。README 中提供了基本设置的说明,以及入门指南。 我们将介绍三种传统的不同类型的 CSRF:
1.资源包含
2.基于表单
3.XMLHttpRequest
资源包含是在大多数介绍 CSRF 概念的演示或基础课程中可能看到的类型。这种类型归结为控制 HTML 标签(例如<image>、< audio>、<video>、<object>、<script>等)所包含的资源的攻击者。如果攻击者能够影响 URL 被加载的话,包含远程资源的任何标签都可以完成攻击。
由于缺少对 Cookie的源点检查,如上所述,此攻击不需要 XSS,可以由任何攻击者控制的站点或站点本身执行。此类型仅限于 GET 请求,因为这些是浏览器对资源 URL 唯一的请求类型。这种类型的主要限制是它需要错误地使用安全的 HTTP 请求方式。
我们将讨论的第二种类型是基于表单的 CSRF,通常在正确使用安全的请求方式时看到。攻击者创建自己的表单,模仿他们想要受害者提交的表单; 它包含一个 JavaScript 片段,强制受害者的浏览器提交表单。该表单可以完全由隐藏的元素组成,并且表单应该迅速地提交,以致受害者不能发现它。由于处理 cookies,攻击者可以在任何站点上发动攻击,只要受害者使用有效的 cookie 登录,攻击就会成功。如果请求是有目的性的,成功的攻击将使受害者回到他们平时正常的页面。该方法对于攻击者可以将受害者指向特定页面的网络钓鱼攻击特别有效。
我们将讨论的最后一个主要类型是 XMLHttpRequest(XHR)。由于需求的需要,这可能是最不可能看到的。由于许多现代 Web 应用程序依赖 XHR,我们将花费大量的时间来构建和实现这一特定的对策。基于 XHR 的 CSRF 通常由于 SOP 而以 XSS 有效载荷的形式出现。没有跨域资源共享策略(CORS),XHR 仅限于攻击者托管自己的有效载荷的原始请求。这种类型的 CSRF 的攻击有效载荷基本上是一个标准的 XHR,攻击者已经找到了一些注入受害者浏览器 DOM 的方式。
以下解决方案是可以用于实际部署的极大简化。它主要侧重于客户端和 token 管理。对请求和响应的拦截和修改有许多奇怪的边缘情况,需要大量关于本身运行平台的知识。理解平台的复杂性有很大的权衡,以避免理解和处理 CSRF 本身的复杂性。理想情况下,最好的解决方案是使用一个框架来提供内置和利用 CSRF 保护的框架。尽管有免责声明,但仍然存在许多理由,如下所述的解决方案是有道理的。
现代防御
有许多例子都证明不可能修改应用程序来实现 CSRF 的防护。一方面源代码无法得到,另一方面修改应用程序的风险太高,或者由于应用程序的限制不容易完成。该解决方案特别适合部署在 RASP、WAF、反向代理或负载平衡器中,并且可用于为单个应用程序提供保护,或者使用相同配置的所有应用程序。当部署平台被理解得很好但是应用程序不被适用时,这是特别有用的。我们来讨论通常用于防范 CSRF 的现有解决方案,以及如何根据上述要求构建它们。
首先,正确使用安全和不安全的 HTTP 请求方式很重要。这一点不是一个有效的解决方案,但它会使一切变得更加容易,接下来的两种方法取决于它。不幸的是,没有一个可以在事实之后应用的解决方案。这是在构建应用程序时需要做的,需要设计和架构。幸运的是,大多数现代Web 框架都有一个路由器的概念,它强制要求有个与 HTTP 请求方式配对的终端。在现代框架中,对与终端不匹配的请求会导致错误。如果这是您的应用程序无法实现的,我们稍后将讨论解决方法。
下一个保护是验证请求的来源。该对策旨在确保进入应用程序的请求源自应用程序内的(或具有 CORS 的其他可信来源)。正确的请求方式很重要,因为只要我们假设只有状态改变的请求是不安全的,那么我们只需要验证不安全请求的来源。由于我们上面讨论的问题,验证安全请求的来源是有问题的。如果需要,那么一个解决方案是创建一个已知安全网址的排除列表,例如用户首次访问时将要访问的主页或可能的着陆页。这将防止外部来源的 CSRF,但允许用户到达网站的期望行为,并在首次访问时保持登录状态。
这种保护并不是绝对必要的,但它增加了额外的层次,并且可能是您要使用 CORS 的一种解决方案。由于应用程序中的 SOP 和 token 的分配,CORS 使 token 的实现变得格外困难。源验证还取决于 HTTP 头的存在,但由于浏览器差异,浏览器扩展或某些请求条件而可能不存在的 HTTP 头。如果请求头缺少,则默认选项应始终是打开失败,并依赖不同层的解决方案来减轻 CSRF 。
您可能会想知道的是,鉴于 Referer 欺骗的可能性和易用性,比较 referer 是否可信。有两个部分使这无关紧要。第一个是 Referer 欺骗的唯一方法是直接来自受害者。如前所述,这完全是客户端攻击,所以受害者的浏览器必须有意地伪造 Referer 来绕过检查。这是不太可能故意发生的事情。第二个因素是这些 Headers,Origin 和 Referer 不能被 JavaScript 设置,因为它们受到保护,并且如果攻击者的 XSS 有效负载尝试设置它们将导致错误。这也限制了任何对受害者浏览器上这些头的修改,假设用户永远不会故意去自己攻击自己、浏览器也正常工作,这或许是安全的。
第三个也是最常用的对策是 token。token 有几个不同的种类,但是每种实现最终都使用同步token。要更完整的了解,您应该阅读“双重提交” tokens 和“加密” tokens。尽管此处讨论的解决方案较简单,但双重提交令牌应可用于以下解决方案,而加密令牌通常由于 AES 或其他选择的加密方案的成本而不太有效。相反,我们将使用同步器和加密的混合,提供最佳的两种解决方案。
同步器 tokens 通过使用唯一的 token 让服务器和浏览器同步工作。对一个安全方法的请求服务器会返回一个 token,浏览器会随着每个不安全的请求一起返回给服务器,通常在表单正文或请求头,这具体取决于请求的类型。在允许请求继续之前,服务器验证该 token 是真实的和有效的; 服务器还将提供一个新的 token,以便令牌不会持续重复使用或打开来重复攻击。由于 SOP,这将阻止攻击者控制的主机上的 CSRF 有效载荷。攻击者将无法得到 token 并将其插入到请求中,因为这样做将要求攻击者能够强制受害者向远程站点请求并返回响应 - SOP 恰恰就是被设计来阻止这个的。攻击者唯一可以利用的就是应用程序里的xss跨站脚本。
token 由四部分组成,必须保持完整性才能有效。任何一个的损失将显着削弱 token 的保护。这四个部分是随机数,用户标识符,期限和真实性验证:
1.随机数的关键空间大小并不是太重要,只要它足够大以确保缺少重复。
2.用户标识符可以是用户唯一的任何值。在我们的实现中,我们将选择使用会话标识符。
3.寿命或到期时间是 token 有效的长度。理想情况下,您希望时间足够短,以至于被盗后不能长时间使用,但长度足以使真实用户使用它时不会过期,从而导致失败的请求。在大多数框架实现中,通常将 token 保存在 session 中并且随着 session 的超期而失效。这样做只有一个值在任何一个时间都有效,在我们的情况下不是这样,所以需要离散的到期。在我们的例子中,我们会默认一个小时。
4.一定有办法保证 token 是真实的,并没有被篡改或伪造。在框架内实现的解决方案通常可以通过将该值存储在用户永远无法访问的服务器端会话存储中来。在我们的例子中,我们将依靠HMAC-SHA256,并提供一个可以验证的 token 的签名。这具体是另一个原因,因为攻击者还需要获取 HMAC 的密钥以伪造令牌。如果密钥被破坏,整个 token 和密钥空间是不相关的在这种情况下,随机性只是为令牌值提供一些额外的熵,以最小化被盗或泄漏令牌的有用性。这也是我们如何避免大多数框架依赖于会话存储的需求,同时获得比大多数加密令牌解决方案有更好的性能。
token的实现有两个方面,服务器端处理token的生成/验证以及客户端,客户端将token发送到服务器以获取需要的请求。除了提供生成和验证的示例之外,我们不会深入到服务器端实现中,正如之前在声明中所说,处理拦截请求/响应的具体细节因平台而异。只需说一下,深入特定平台的中间件API,并使用它来实现接近以下步骤的操作。
1.当前session是否有token?如果没有,请标记应生成token。
2.请求是否需要验证?如果是,验证并标记该token已被使用。
3.如果需要验证并失败,那么短路响应并停止处理。如果验证成功,则处理请求。
4.如果token不存在或被标记为已使用,则生成新token并将添加其cookie到响应中。
值得注意的是,每次生成一个新的token,即使没有验证,也不会增加任何安全性或者打开一个新的攻击向量。如果更容易,您可以在每个请求上生成一个新的令牌来构建您的实现。您将不会获得额外的保护,但是性能损失应该可以忽略不计。token被添加到cookie中,作为一种为浏览器提供值的方式,javascript 可以访问到它,浏览器也会自动的保存它。确保 HttpOnly标志永远不会用于此 cookie 这很重要。这样做会打破实施,但是没有任何安全问题,因为唯一的威胁来自 XSS,它提供了必要的条件来绕过 CSRF 的保护。
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 }
以上是创建新 token 的简单示例。只是被连字符连接起来的四个部分。HMAC 将前三部分用于加密,以确保每个人的真实性,加密后的结果作为第四部分。选择连字符作为分隔符,因为冒号不是 Cookie 版本0 Cookie 的有效字符。使用它必须要升级到版本1,这可能会破坏与旧浏览器的一些兼容性。
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 }
上面的代码块是一个验证 token 并计算有效性的简单示例。token 被分为四个部分,第一步是通过前三个部分重新生成 HMAC 并将其与期望的 HMAC 进行比较来验证 HMAC 。确保在这里使用一个恒定的时间来避免引入任何时序攻击。如果成功,我们验证token是否过期,用户是否匹配。从根本上来说这就是生成和检验 token 的流程。真正的威胁是用户的浏览器自动提交请求时也带上了 token。
大多数现代框架在构建应用程序时都会为您考虑到了这一点。他们有库函数来处理 XHR,将token 插入到请求和模板助手中,以便将当前 token 包含在表单中。这是我们要模仿的功能:不依赖于框架为我们提供。相反,作为我们回应拦截的一部分,我们将在响应中添加或附加一小段 JavaScript。尽管严格测试绝对不符合规范,但几乎每个浏览器都将正确处理脚本标签,JavaScript 中分别添加或附加到打开或关闭 HTML 标签。我们将专门针对 HTML contentType的响应,以确保我们只将注入到我们不会中断的响应中,我们只修改非 XHR 的响应。这将避免我们将脚本多次加载到浏览器或 JSON 响应中。
实现这一点有两个部分,一个是处理表单提交,另一个是处理 XHR 。第一个代码段是附加到onclick 事件的文档回调最小化版本。将它附加到文档而不是尝试附加到单个表单或可点击元素很重要,因为在附加时,表单或元素很可能不存在于 DOM 中,导致回调未触发。相反,我们附加到始终存在的文档,并委托给我们关心的元素。我们还需要使用 onclick 而不是onsubmit,因为 onsubmit 在所有浏览器和版本中都不会浮动,这意味着我们无法附加到文档并被调用。
var target = evt.target; while (target !== null) {
if (target.nodeName === 'A' || target.nodeName === 'INPUT' ||
target.nodeName === 'BUTTON') {
break;
}
target = target.parentNode;
} // We didn't find any of the delegates, bail out if (target === null) {
return; }
第一节抓取被触发事件的目标元素。这是用户点击的元素。由于 DOM 的树结构和事件冒泡系统,这个元素可能不是我们感兴趣的元素,而是我们必须走出 DOM 寻找可以提交表单的元素; 在这种情况下,比如:<a>、<input> 或 <button> 标签。如果我们在发现一个 DOM 之前到达 DOM 的顶端,那么就轻松了,因为它是一个没有提交表单的元素上的点击事件。
// If it's an input element make sure it's of type submit var type = target.getAttribute('type'); if (target.nodeName === 'INPUT' && (type === null || !type.match(/^submit$/i))) {
return;
} // Walk up the DOM to find the form var form; for (var node = target; node !== null; node = node.parentNode) {
if (node.nodeName === 'FORM') {
form = node; break;
}
} if (form === undefined) {
return; }
接下来我们检查标签是否是 <input> 。如果是,那么我们要确保它是一个提交按钮。否则它不会提交表单 , 而只是使浏览器关注元素。一旦我们确定目标导致提交事件的发生,那么继续从DOM 中寻找一个表单标签。如果我们到达 DOM 的顶端,但没有找到一个表单标签,那么该元素不会被提交,除非它使用 XHR,这将被 XHR 相关代码部分处理。
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);
一旦找到表单,剩下的唯一步骤就是把这个 token 添加到 form 中作为一个隐藏的输入元素。第一步是从先前的提交中检查元素是否已经存在。如果是,请检查该值,并在必要时进行更改。如果没有,则创建一个新元素并将其附加到表单中。由于冒泡的作用方式,此处理程序在提交表单之前触发,并在处理程序返回之前将元素添加到表单中,导致浏览器提交的请求带有表单中的新元素,然后将token添加到正文的请求。 对于非基于表单的请求,需要一种将token存入 XHR 请求的方法。大多数库都提供抽象方法,包括 jQuery,这使得这更容易,因为它们提供了可以修改请求的回调函数,允许不同的请求。不幸的是,我们不能假设一个特定的库将会出现,并且需要为标准的 XHR API 创建我们自己的 hook。为了做到这一点,我们将包装和修补对象本身以添加额外的功能。
XMLHttpRequest.prototype._send = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(){
if(!this.isRequestHeaderSet('X-Requested-With')){
this.setRequestHeader('X-Requested-With','XMLHttpRequest'); } var tokenValue = getCookieValue('CSRF-TOKEN');
if(tokenValue!== null){
this.setRequestHeader('X-CSRF-Header',tokenValue);} this._send.apply(this,arguments); };
通过利用 JavaScript 的原型继承和动态性质,我们将原始发送方法的副本保存到对象上,以便我们可以保留对其的引用以供稍后使用。然后,我们创建一个附加到发送原型的新函数,该原型从 cookie 中提取 token,并向请求中添加一个带有值的 header 。真正的发送方法通过保存的 referer 来调用,原始参数通过它们按预期工作。
就浏览器中的代码而言,认为 API 没有改变,XHR 对象也并没有不同,但是我们现在强制所有请求都在服务器可以读取的 header 中提交一个 CSRF token。 这个实现的一个特别的注意事项是,由于原型支持和 XHR 可用性,它只能用于 Internet Explorer(IE)8。XHR 被引入 Internet Explorer 7,但是不存在正确的原型支持,需要额外的解决方案来完善此功能。至少 IE6 可以使用基于表单的解决方案。
可能有一种方法可以通过自定义的 ActiveX 控件为旧版本的IE版本添加额外的支持,但不在本文的范围之内。处理旧版本浏览器缺乏支持的另一个解决方案是简单地不执行 CSRF 检查,而是检查用户代理头。
尽管如此,这些可能并不总是存在并且可能被伪造,这样做将需要受害者主动地忽略安全性问题或已经被恶意软件或 XSS 攻击而泄密。所有其他浏览器似乎都有很好的支持,如下图所示。上面的代码可能会被比以下版本更老的浏览器支持,但是,查找测试副本很难,这些版本的浏览器也覆盖了大多数人常用的。
关于未来
现在我们已经根据当前的实践构建了涵盖一个适用于旧版本浏览器到现代浏览器的解决方案,现在是时候来看看一个新的解决方案,这可能是大多数CSRF案例的完结了。
这是一种扩展名为 Same-Site 的 Cookie 的扩展形式,它增加了对 Cookie 源的检查。Same-Site 允许浏览器限制只发送来自与域匹配的主机的请求发送的 cookie,大大地取代了对同步器令牌的需求。有两种形式,strict 和 lax。strict 会检查所有安全和不安全的请求,而 lax 只支持检查不安全的请求。大多数应用程序将需要配置为 lax,因为保护安全请求就不允许将会话 cookie 与原始 GET 请求一起发送到站点。
在撰写本文时,浏览器支持非常之少,主要是 Chrome 支持该功能。下表列出了从这里得到的信息。但是,作为扩展程序,Same-Site不会破坏不支持旧浏览器对 Cookie 的兼容性。较老的浏览器将会自动忽略此项 功能。
在撰写本文时,Same-Site 还只是以草案存在,我不知道有任何可以支持这种功能的 Cookie库。只有这种方式变得更稳定和被多数人接受,才可能像token一样地使用。它可以与同步器token 结合使用,以支持较旧和较旧的浏览器。
Same-Site 的一个缺点是缺乏 CORS 支持。在撰写本文时,没有提及添加对白名单特定来源的支持,以安全地发送 cookies。这将会破坏依赖于向服务器提供状态信息的 Cookie 的 CORS 请求。一个潜在的解决方法是删除仅用于不使用 Same-Site 并执行源验证的外部站点的第二个 cookie
以上是关于Wiping Out CSRF的主要内容,如果未能解决你的问题,请参考以下文章
What is the best way to handle Invalid CSRF token found in the request when session times out in Spr