我开始了解CSRF令牌是如何生成和验证的。我是通过在Phoenix web应用程序中跟踪函数调用流来做到这一点的。这是一个让我陷入一些深洞的过程,但最终是一次有益的经历。对本文的具体细节不太感兴趣的读者可以跳到底部的摘要部分查找TL;
本文详细介绍了CSRF令牌的生命周期。为了开始我们的旅程,我们将导航到正文中包含HTML表单的页面-例如,/login&34;。我们将检查表单中的字段,以查看是否有HIDDED_CSRF_TOKEN字段。它是自动放入表格中的。
我们可以看一下Phoenix.HTML.form_tag/3的源代码,看看魔术是在哪里发生的。这是代码中添加隐藏输入字段以存储令牌的部分。当方法是";POST";时,它会添加到表单中。
进一步跟随代码,我们看到Plug.CSRFProtection.get_CSRF_TOKEN_FOR/1用于生成令牌。下面是从IEX交互式控制台调用函数示例。
现在,这看起来就像是一堆随机字符(剧透提醒:它有点像是一堆随机字符)。让我们更多地跟踪代码,以了解它是如何工作的,以及为什么它看起来是这样的。
结果是GET_CSRF_TOKEN_FOR/1执行了一些分支,并最终调用了Plug.CSRFProtection.Get_CSRF_TOKEN/0。根据文档字符串,它的工作是生成一个令牌并将其存储在流程字典中(如果该令牌还不存在)。令牌的生成方式如下所示:
我们将暂时忽略掩码函数,并跳过几个中间函数调用来查看Plug.CSRFProtection.Generate_Token/0,这就是我们最终进入正题的地方。
有一个对Erlang函数的调用:crypt.strong_rand_bytes/1,它将生成N个随机字节。我们将不再深入了解此函数的实际工作原理。随机数本身可能是另一篇文章。
然后对随机字节进行Base64编码。我已经写了一篇关于Base64如何工作的文章,所以我们也不会深入探讨这一点。可以说,将随机字节编码成可以使用HTTP安全传输的格式是有意义的。
现在我们已经了解了令牌是如何创建的:它是一个base64编码的随机字符串。然而,乐趣并不仅止于此,因为这只是一个未加掩饰的代币。未屏蔽的令牌已传递给另一个函数Plug.CSRFProtection.ask/1。
哇,现在这事变得有趣了。我们将不得不更深入地研究Plug.Crypto模块来了解这里发生了什么。让我们来计算一下:
要回答第一个问题,我们将深入到Plug.Crypto模块并查看MASK/2。我们稍后将返回到Plug.CSRFProtection.ask/1。
@doc";";";";";";";";";";这两个令牌要求大小相同。";";";@spec ask(inary(),inary())::inary()def ask(Left,Right)do MASK(Left,Right,";";)enddefp MASK(<;<;x,Left:Binary>;&Get。,acc)执行掩码(左、右;<;<;acc::二进制,x^^y^ygt;>;)enddefp掩码(<;<;>;>;,<;<;>;>;,acc)执行ACCEND。
不要让递归、二进制模式匹配和^^宏吓到您!^是Bitwise模块中的XOR运算符。MASK/2函数递归地对令牌和掩码中的每个字符进行异或运算。没有太多的业务逻辑情况需要XOR,所以这不是我们每天都要用到的。
假设我们想要手动进行XOR;C&34;和";d&34;。首先,我们找到字符的ASCII值,即67和100。然后我们将ASCII值转换为二进制,即1000011和1100100。接下来,我们将二进制值排成一行,以便比较每一位。
XOR的规则是必须有一个,但不能两个都有,才能产生1。例如:
最后,我们将结果0100111转换回10进制数字,然后找到相应的ASCII码字符。";';";是";C";XOR";d";的结果。有了这些知识,我们可以更好地理解Plug.Crypto.ask/2在做什么。
该函数接受一个令牌,然后生成一个掩码。这有点令人困惑,因为掩码函数本身也有一个名为MASK的变量。请注意,掩码(变量)的生成方式与令牌相同,因此实际上可以将其视为第二个令牌。标记和掩码变量一起进行异或运算,然后进行base64编码。最后,还将掩码变量附加到它的所有部分。
我们现在知道面具是什么了,但是为什么有必要这么做呢?仍然屹立不倒。
让我们看看是否有任何证据证明为什么会有这个功能存在,所以我们将在GitHub上打开一个git指责视图。
啊-哈!";屏蔽CSRF令牌以避免入侵攻击";-这是一个有用的提交消息!通过对什么是漏洞攻击进行一些研究,我们可以了解到,它基本上是攻击者能够发送一系列请求,并逐渐计算出响应正文的一部分,即使响应是加密的。
攻击者利用响应使用压缩的事实,并且压缩响应的大小要么增大,要么缩小,这取决于对字符的一个小猜测是正确的还是不正确的。在SSL中,发现该漏洞的安全研究人员在30秒内就消失了,他们非常详细地解释和演示了该攻击。
他们还发布了一篇论文,提出了攻击缓解方案。本摘录解释了Plug.CSRFProtection.ask/1';的算法。
攻击依赖于目标密码在请求之间保持不变的事实。虽然在每个请求上轮换秘密通常是不切实际的,但汤姆·伯森(Tom Berson)提出了一种方法,可以综合这一效果。不是在页面中嵌入秘密S,而是在每个请求上生成新的一次性便签P,并在页面中嵌入P||(P⊕S)。这里,我们使用||表示串联,使用⊕表示异或。
掩蔽令牌有助于防止攻击者逐渐猜测响应正文的字符。这是因为即使令牌在不同的请求之间不会改变,掩码也会改变。因此,令牌每次在响应正文中都以不同的方式表示。当我们开始查看令牌验证时,我们将看到XOR掩码可以反转以显示原始令牌。
现在我们知道了为什么屏蔽令牌,以及屏蔽它意味着什么。我们还知道如何创建未屏蔽的令牌。然而,仅仅在HTML文档中插入一个令牌是不够的。同样的令牌也放在用户的浏览器cookie中。
如果我们看一下浏览器中的cookie,我们会看到一些东西,但它显然与我们在HTML表单中看到的令牌不匹配。这是因为凤凰卫视对饼干进行了加密签名,这样它们就不会被篡改。
每个Phoenix应用程序都有一个在config/config.ex中定义的SECRET_KEY_BASE值,这就是菲尼克斯用作签名的地方。但是,SECRET_KEY_BASE应该是没有人知道的保密值,因此不能直接使用SECRET_KEY_BASE。相反,Phoneix依赖于Plug.Crypto.KeyGenerator模块。以下是文档的摘录,总结了Plug.Crypto.KeyGenerator模块的用途:
…。这使应用程序具有单一的安全密钥,但避免在多个不兼容的上下文中重用该密钥。
现在,我们将了解应用程序如何保护会话cookie数据(包括CSRF令牌)的完整性和真实性。
怎么可能在潜在不安全的地方使用应用程序的密钥呢?Plug.Crypto.KeyGenerator通过实现称为基于密码的密钥派生函数2或PBKDF2的算法来实现这一点。
它以盐和秘密作为输入。然后,它重复应用伪随机函数(PRF)来创建派生密钥。PBKDF2的一个重要细节是,它允许程序指定将应用PRF的次数。换句话说,它允许程序决定算法应该花费多长时间。这有助于降低暴力攻击的可行性。迭代计数默认为1,000,这是推荐的最小值。
这里使用的PRF是HMAC-sha256。巧合的是,HMAC也用于整体签名和验证会话数据,我们将在下一节中介绍这一点。
通过记录Plug.Crypto.KeyGenerator.Generate/6的结果,我们可以看到它只生成一次会话签名密钥。所有后续请求都使用一个缓存值,该值存储在ETS中。下面是一个示例(位串是结果,也称为派生的会话签名密钥):
iex(2)>;:ets.lookup(Plug.Keys,{";2/JWt8kJK5ybWYFPqXGDZj0o3LuKerv1CnG/F8LVbLi71hZTYllzKxP9HMpT+y0m";,";8yQvCfAG";,.(2)>;1000,32,:sha256})[{{";2/JWt8kJK5ybWYFPqXGDZj0o3LuKerv1CnG/F8LVbLi71hZTYllzKxP9HMpT+y0m";,";8yQvCfAG";,1000,32,:sha256},<;<;125、248、227、17、106、91、67、35、35、99、173、58、14、29、96、107、220、193、148、164、44、239、17、58、110、9、116、230、91、9、188、88。
既然我们可以看到会话签名密钥是如何从应用程序的SECRET_KEY_BASE中生成的,那么我们就可以了解它是如何用于实际签名的。
如上所述,HMAC不仅用于生成签名密钥,还用于对会话cookie数据进行签名。正如我们将看到的,签署数据的目标不是防止数据被看到,而是验证数据的真实性和完整性。
HMAC的输出是一个字节字符串,它反映了作为函数输入提供的消息和密钥。该消息与HMAC一起发送。其想法是消息不能更改,因为HMAC将不再匹配。HMAC也不能更改,因为秘密密钥是未知的(取决于底层散列函数的强度)。
我们可能会考虑的一个问题是,为什么不直接使用SHA哈希函数,或者使用其他类型的校验和方法呢?答案是HMAC优于SHA-1和SHA-256,因为它可以防止长度扩展攻击。Computerphile有一段关于HMAC的很棒的视频。
在本例中,消息是带有CSRF令牌的ELEXIR映射。密钥是我们在上面讨论的派生会话签名密钥,底层散列函数是sha256。
函数hmac_sha2_sign/3由Plug.Crypto.MessageVerifier.sign/3调用,我们可以查看它来理解会话cookie为什么是这样的。
#`payload`和`key`为二进制文件,digest_type为:sha256defp hmac_sha2_sign(payload,key,digest_type)do tected=HMAC_sha2_to_tected(Digest_Type)PLAN_TEXT=sign_input(Protected,payload)Signature=:crypt.hmac(digest_type,key,common_text)encode_Token(PLAN_TEXT,Signature)end。
此函数的结果将是我们可以在浏览器中看到的Cookie的值:
请注意,此文本字符串可以在";.";';之间拆分为三个部分。第一个部分是hmac_sha2_to_tected(Digest_Type)的结果。它只是字符串HS256&34;的Base64编码格式。
第二部分也是用Base64编码的。它表示正在签名的消息,来自Signing_Input(受保护,有效负载)。我们可以通过解码,然后将二进制文件翻译成一个项来查看它:
最后一部分是签名,它来自于:crypt.hmac(digest_type,key,PLACE_TEXT)。正如我们从函数参数中看到的,它需要一个散列函数(Digest_Type)、一个密钥,然后是要保护的消息。签名也是Base64编码的,并与消息结合在一起。
现在我们可以看到令牌被安全地存储在cookie中,以及它是如何创建的细节,最后要看的是数据是如何验证的。
我们已经看到,令牌被放置在HTML文档和Cookie中。最后要介绍的是它们是如何被验证的。在下面的代码中,我们可以从Plug.CSRFProtection模块开始跟踪代码,看看令牌验证是如何发生的。
defp VERIFIED_REQUEST?(CONN,CSRF_TOKEN,ALLOW_HOSTS)在@UNPROTECTED_METHANDS||VALID_CSRF_TOKEN?(CONN,CSRF_TOKEN,Conn.body_params[";_CSRF_TOKEN";],ALLOW_HOSTS)||VALID_CSRF_TOKEN?(CONN,CSRF_TOKEN,FIRST_x_CSRF_TOKEN(CONN),ALLOW_HOSTS)||SKIP_CSRF。
这两个参数都被发送到VALID_CSRF_TOKEN?/4,然后发送到VALID_MASTED_TOKEN?/3。掩码与HTML正文中令牌尾部的模式匹配。然后,Plug.Crypto.MASKED_COMPARE/3能够比较令牌是否匹配。
@doc";";";";";";";";";";@spec MASKED_COMPARE(BINARY(),BINARY(),BINARY())::boolean()def MASKED_COMPARE(LEFT,RIGHT,MASK)当IS_BINARY(LEFT)和IS_BINARY(RIGHT)AND IS_BINARY(MASK)DO BYTE_SIZE(LEFT)==BYTE_SIZE(右)AND MASKED_COMPARE(左,右,掩码,0)enddefp MASKED_COMPARE(<;<;x,LEFT:BINARY>;>。<;z,MASK::BINARY&>;&>,acc)do xorred=x^^^(y^^z)MASKED_COMPARE(LEFT,RIGHT,MASKED_COMPARE(LEFT,RIGHT,MASKED,ACC|xorred)enddefp MASKED_COMPARE(<;>;,<;<;>;,<;<;>;,acc)。
作为总结,让我们快速回顾一下用户访问具有HTTP方法POST&34;表单的页面的整个过程。事件的顺序并不意味着完全准确-我在这里的目标是勾勒出总体上发生的事情的概括性图景。
当涉及到保护CSRF时,Phoneix似乎使用了双重提交Cookie的方法。
生成页面的HTML标记。因为Form方法是POST,所以Phoenix.HTML.form_tag检测到登录表单应该包括一个CSRF令牌。
调用Plug.CSRFProtection.get_csrf_token_for(";/login";),它会生成一个屏蔽的CSRF令牌。掩码是将令牌和随机字节进行XOR运算的结果。
如果进程字典尚不存在,则会将相同的令牌添加到该进程字典中。
从应用SECRET_KEY_BASE中导出密钥,以便对存储在Cookie中的数据进行密码签名。这是为了防止数据被应用程序本身以外的其他人更改。这是通过PBKDF2完成的。
Cookie中的数据(即CSRF令牌)是Base64编码的,并与HMAC-sha256函数的输出结合在一起。这有助于防止数据被篡改。
应用程序使用HTML标记响应用户浏览器,该标记在表单正文中包含注入的CSRF令牌,并且还使用Set-Cookie响应头在用户浏览器上设置具有相同(未屏蔽)令牌的Cookie。
此时,用户可以填写表单并提交。这将触发HTTP POST请求,并验证令牌。
验证会话数据以确保其未被修改。如果成功,则显示/返回未屏蔽的CSRF令牌。
将来自HTML正文的掩码令牌与来自会话的未掩码令牌进行比较。可以从HTML正文中的标记检索正确的掩码。比较以防止计时攻击的方式进行。
如果代币匹配,那么就不会有问题。如果令牌不匹配,则会发生错误。
仅此而已!正如我们已经看到的,作为这个过程的一部分,有很多奇特的东西正在进行,尝试并理解它们是如何结合在一起的,这是相当令人惊讶的。
正是这类东西让我真的很欣赏人们为制作和破坏软件所做的所有工作。我认为偶尔仔细研究一下我们使用的技术、库和框架是一个很好的实践。这是一件幸事,我们不必完全理解所有的细节就能创造并富有成效,但时不时地这样做肯定是有价值的。