我最近在写《 Understanding Asynchronous JavaScript 》时开始研究网络安全——我想确保我的建议是安全的,并且我的建议不会对我的任何学生造成伤害。
不幸的是,安全领域的文章很难理解。文章中有很多词会引发很多恐惧、不确定和怀疑。当我读到这些文章时,我会感到恐慌——我担心我最终可能会做错事——尽管这些文章的意图是好的!
许多文章也没有披露完整的 CSRF 细节,如何设置 CSRF 攻击,以及如何防止 CSRF 攻击,这让我对我学到的东西产生了怀疑。我最终不得不自己解决问题。
我想让你更容易理解 CSRF,所以我尝试写一篇文章,其中包含有关 CSRF 攻击的完整(和逐步)信息。我希望这篇文章能让你清楚和自信地构建安全的 Web 应用程序。
两种 CSRF 攻击
有两种 CSRF 攻击:
- 正常 CSRF 攻击
- 登录 CSRF
我们将首先讨论普通 CSRF 攻击,然后是登录 CSRF。
什么是 CSRF 攻击
CSRF 攻击是一种诱使受害者向他们进行身份验证(登录)的网站提交恶意请求(他们不打算提出的请求)的攻击。
该请求必须来自另一个名为“Cross-Site”的网站。此请求还模拟了经过身份验证的用户,这将其命名为“Request Forgery”。
CSRF 攻击是盲目的——这意味着攻击者看不到受害者提交请求后会发生什么。因此 CSRF 攻击通常针对服务器上的状态更改。
什么是状态变化?基本上,任何修改数据库的事情都是状态变化。状态更改的示例包括:
- 更改用户名和密码
- 汇款到账户
- 从用户帐户发送虚假消息
- 分享来自用户帐户的不当图片或视频
CSRF 攻击利用了浏览器在每个请求中自动向服务器发送 cookie 的事实。如果没有任何 CSRF 保护,服务器可能会在存在身份验证 cookie 时假定请求是有效的。
身份验证 cookie 可以是任何东西,只要服务器使用它们来检查用户是否有效。它可以是访问令牌。它也可以是会话 ID。这取决于服务器如何处理身份验证。
CSRF 攻击起作用的先决条件
CSRF 攻击成功需要四个先决条件。
- 任何方法的请求都会发送到服务器。
- 用户必须经过身份验证。
- 服务器必须将身份验证信息存储在 cookie 中。
- 服务器没有实现 CSRF 预防技术(将在下面讨论)。
CSRF 攻击的工作原理
在攻击者可以发起 CSRF 攻击之前,他们需要找到他们可以定位的一致请求。他们必须知道请求的作用。这可以是任何请求——GET、POST、PUT 或 DELETE。什么都行。
一旦他们选择了目标请求,他们就必须生成一个虚假请求来欺骗用户。
最后,他们必须欺骗用户发送请求。大多数时候,这意味着:
- 寻找一种在用户不知情的情况下自动发送请求的方法。最常见的方法是通过图像标签并自动提交 JavaScript 表单。
- 歪曲链接(或按钮),诱使用户点击它。 (又名社会工程)。
通过 GET 请求进行攻击
仅当服务器允许用户使用 GET 请求更改状态时,使用 GET 请求的 CSRF 攻击才有效。如果您的 GET 请求是只读的,则不必担心这种类型的 CSRF 攻击。
但是,假设我们有一个服务器不遵循编程最佳实践并允许通过 GET 请求更改状态。如果他们这样做,他们就有麻烦了——大麻烦。
例如,假设有一家银行允许您使用以下端点转账。您只需在 GET 请求中输入account
和amount
即可向某人汇款。
https://bank.com/transfer?account = Mary&amount = 100
攻击者可以生成一个链接,将钱发送到他们的账户。
# Sends 9999 to the Attacker's account https://bank.com/transfer?account = Attacker&amount = 9999
此时,攻击者可以想办法在用户不知情的情况下自动触发链接。
一种方法是将链接包含在网页或电子邮件中的 0x0 图像中。如果用户访问此网页或电子邮件,则 GET 请求会自动触发,因为浏览器和电子邮件已配置为自动获取图像。
(现在我明白了为什么电子邮件提供商会禁止加载图像以作为安全预防措施)。
<!-- Downloading this image triggers the GET request attack --> <img src= "https://bank.com/transfer?account=Attacker&amount=9999" width= "0" height= "0" border= "0" />
另一种方法是歪曲链接的作用。这是因为人们在点击链接之前不会检查链接。如果此人单击该链接,他们会在不知情的情况下向攻击者发送 GET 请求。
<!-- Fake link that triggers the GET request attack --> <a href= "https://bank.com/transfer?account=Attacker&amount=9999" > View my Pictures </a >
如果用户通过身份验证,服务器将收到一个身份验证 cookie,使其相信请求是有效的。如果服务器没有使用任何 CSRF 保护机制,钱将被发送给攻击者。
GET CSRF 攻击示例:
- uTorrent 在2008 年遭受了 CSRF 攻击,它允许通过 GET 请求更改状态。
- Youtube 在2008 年曾存在一个安全漏洞,允许攻击者执行用户几乎所有可能的操作,包括发送消息、添加到好友列表等。
如果您单击上面的链接。您将能够找到创建此类 CSRF 攻击的真实 GET 请求的示例。 (别担心,这里没有奇怪的链接?)。
带有 POST 请求的 CSRF 攻击
带有 POST 请求的 CSRF 攻击遵循相同的模式——但它们不能通过链接或图像标签发送。它们需要通过表单或 JavaScript 发送。
假设我们有相同的易受攻击的端点,攻击者只需输入account
和amount
信息即可触发请求。
POST https://bank.com/transfer?account = Attacker&amount = 9999
攻击者可以创建一个表单并向用户隐藏account
和amount
值。单击此虚假表格的人将在他们不知情的情况下发送 POST 请求。
<!-- Form disguised as a button! --> <form action= "https://bank.com/transfer" method= "POST" > <input type= "hidden" name= "acct" value= "Attacker" /> <input type= "hidden" name= "amount" value= "9999" /> <button> View my pictures </button> </form>
这个表单也可以在人们不知道的情况下用 JavaScript 自动执行——真正的用户甚至不需要点击按钮,但他们已经遇到了麻烦。
<form> ... </form> <script> const form = document . querySelector ( ' form ' ) form . submit () </script>
POST CSRF 攻击很可怕,但有一些方法可以防止它们。我们将在下面的预防部分讨论这些技术。
带有 PUT 和 DELETE 请求的 CSRF 攻击
CSRF 攻击不能通过PUT
和DELETE
请求执行,因为我们使用的技术不允许它们这样做。
是的。你没看错。
CSRF 攻击无法通过 HTML 表单执行,因为表单不支持PUT
和DELETE
请求。它只支持GET
和POST
。如果您使用任何其他方法( GET
和POST
除外),浏览器会自动将它们转换为 GET 请求。
<!-- Form doesn't send a PUT request because HTML doesn't support PUT method. This will turn into a GET request instead. --> <form action= "https://bank.com/transfer" method= "PUT" ></form>
所以你永远不能通过 HTML 来执行 CSRF 攻击。
现在还有一个有趣的问题:如果 HTML 不允许,人们如何通过表单发送PUT
和w
请求?经过一番研究,我发现大多数框架都允许您发送带有_method
参数的POST
请求。
<!-- How most frameworks handle PUT requets --> <form method= "post" ... > <input type= "hidden" name= "_method" value= "put" /> </form>
您可以通过 JavaScript 执行PUT
CSRF 攻击,但当今浏览器和服务器中的默认预防机制使得这些攻击很难发生——您必须故意让防御措施发生。
这就是为什么。
要执行PUT
CSRF 攻击,您需要使用put
方法发送 Fetch 请求。您还需要包括credentials
选项。
const form = document . querySelector ( ' form ' ) // Sends the request automatically form . submit () // Intercepts the form submission and use Fetch to send an AJAX request instead. form . addEventListener ( ' submit ' , event => { event . preventDefault () fetch ( /*...*/ , { method : ' put ' credentiials : ' include ' // Includes cookies in the request }) . then ( /*...*/ ) . catch ( /*...*/ ) })
由于三个原因,这行不通。
首先,由于 CORS,该请求不会被浏览器自动执行。除非——当然——服务器通过允许来自具有以下标头的任何人的请求来创建漏洞:
Access-Control-Allow-Origin: *
其次,即使您允许所有来源访问您的服务器,您仍然需要一个Access-Control-Allow-Credentials
选项,以便浏览器将 cookie 发送到服务器。
Access-Control-Allow-Credentials: true
第三,即使您允许将 cookie 发送到服务器,浏览器也只会发送将sameSite
属性设置为none
的 cookie。 (这些也称为第三方 cookie)。
如果您不知道我在说什么关于第三点,那么您是安全的 — 如果您将身份验证 cookie 作为第三方 cookie 发送,那么您真的必须是一个恶意的开发人员,想要搞砸您的服务器。
本节内容很丰富。我创建了几篇文章来帮助您准确了解正在发生的事情——以及为什么将自己暴露在PUT
CSRF 攻击中如此令人难以置信的困难:
简而言之——你只需要担心POST
CSRF 攻击,除非你真的搞砸了你的服务器。
CSRF 预防方法
当今最常见的 CSRF 预防方法是:
- 双重提交 Cookie 模式
- Cookie 到标头方法
两种方法都遵循相同的公式。
当用户访问您的网站时,您的服务器必须创建一个 CSRF 令牌并将其放置在浏览器的 cookie 中。此令牌的通用名称是:
- CSRF-TOKEN
- X-SRF-TOKEN
- X-XSRF-TOKEN
- X-CSRF-TOKEN
使用您喜欢的任何令牌名称。他们都工作。
重要的是 CSRF 令牌必须是随机生成的加密强字符串。如果您使用 Node,则可以使用crypto
生成字符串。
import crypto from ' crypto ' function csrfToken ( req , res , next ) { return crypto . randomBytes ( 32 ). toString ( ' base64 ' ) }
如果你使用 Express,你可以像这样将这个 CSRF 令牌放在你的 cookie 中。这样做时,我建议也使用sameSite
strict 选项。 (我们稍后会讨论sameSite
)。
import cookieParser from ' cookie-parser ' // Use this to read cookies app . use ( cookieParser ()) // Setting CSRF Token for all endpoints app . use ( * , ( req , res ) => { const { CSRF_TOKEN } = req . cookies // Sets the token if the user visits this page for the first time in this session if ( ! CSRF_TOKEN ) { res . cookie ( ' CSRF_TOKEN ' , csrfToken (), { sameSite : ' strict ' }) } })
您使用 CSRF 令牌的方式会根据您是否支持双 cookie 提交模式或 Cookie 到标头方法(或两者)而改变。
双重提交 Cookie 模式
这个模式的名字有点误导——因为它似乎意味着用“Double Submit Cookie”发送一个 cookie 两次。
这实际上意味着:
- 您在 cookie 中发送 CSRF 令牌
- 您使用 CSRF 令牌呈现
<form>
– 这将包含在表单的提交中。
(因此双重提交)。
如果你使用 Express,你可以像这样将 CSRF Token 传递到 HTML 中:
app . get ( ' /some-url ' , ( req , res ) => { const { CSRF_TOKEN } = req . cookies // Render with Nunjucks. // Replace Nunjucks with any other Template Engine you use res . render ( ' page.nunjucks ' , { CSRF_TOKEN : CSRF_TOKEN }) })
然后你可以像这样使用CSRF_TOKEN
:
<form> <input type= "hidden" name= "csrf" value= "" /> <!-- ... --> </form>
然后,服务器可以通过比较两个 CSRF 令牌来检查会话的有效性。如果它们匹配,则意味着请求不是伪造的——因为攻击者无法猜测另一个网站中的 CSRF 令牌值。
// Checks the validity of the CSRF Token app . post ( ' /login ' , ( req , res ) => { const { CSRF_TOKEN } = req . cookies const { csrf } = req . body // Abort the request // You can also throw an error if you wish to if ( CSRF_TOKEN !== csrf ) return // ... })
Cookie 到 Header 方法
cookie 到 header 方法是相似的——除了这是用 JavaScript 执行的。在这种情况下,CSRF Token 必须包含在 cookie 和请求标头中。
在这种情况下,我们需要:
- 设置
credentials
以include
或same-origin
以包含 cookie - 从
document.cookies
中获取 CSRF 令牌并将其添加为请求标头。
这是一个示例请求:
// Gets the value of a named cookie function getCookie () { const match = document . cookie . match ( new RegExp ( ' (^| ) ' + name + ' =([^;]+) ' )) if ( match ) return match [ 2 ] } // Sends the request fetch ( ' /login ' , ( req , res ) => { credentials : ' include ' , headers : { ' CSRF_TOKEN ' : getCookie ( ' CSRF_TOKEN ' ) } })
服务器可以像这样检查 CSRF Token 的有效性:
// Checks the validity of the CSRF Token app . post ( ' /login ' , ( req , res ) => { const { CSRF_TOKEN } = req . cookies const { CSRF_TOKEN : csrf } = req . headers // Abort the request // You can also throw an error if you wish to if ( CSRF_TOKEN !== csrf ) return // ... })
使用库使所有这些变得更容易
我向您展示了如何手动创建和测试 CSRF 令牌,因为我想让您了解该过程。
这个过程已经解决了很多次,所以我们不应该手动完成(除非你正在学习,就像我在这里所做的那样)。
如果您使用 Express,我建议使用csurf库,因为与我在上面的示例中展示的内容相比,它更加健壮和灵活。
SameSite Cookie 属性
在上面的示例sameSite
设置为strict
可确保仅当请求来自同一网站时才将 CSRF 令牌 cookie 发送到服务器。这确保了 CSRF 令牌永远不会泄露到外部页面。
您可以(可选但推荐)在设置身份验证 cookie 时将sameSite
属性设置为strict
。这确保不会进行 CSRF 攻击,因为身份验证 cookie 将不再包含在跨站点请求中。
如果您对身份验证 cookie 使用 set sameSite
to strict
是否需要 CSRF 令牌保护?
在大多数情况下我会说不——因为sameSite
已经保护服务器免受跨站点请求。但是我们仍然需要 CSRF 令牌来防止一种特定类型的 CSRF:登录 CSRF。
您可以在本文中阅读有关 sameSite cookie 的更多信息。
登录 CSRF
就意图而言,登录 CSRF 与普通 CSRF 攻击完全不同。
在登录 CSRF 中,攻击者诱骗用户使用攻击者的凭据登录。一旦攻击成功,用户不注意就会继续使用攻击者的账号。
<form action= "http://target/login" method= "post" > <input name= "user" value= "Attacker" /> <input name= "pass" type= "password" value= "AttackerPassword" /> <button> Submit </button> </form>
他们还可以使用 JavaScript 自动触发表单。
const form = document . querySelector ( ' form ' ) // Sends the request automatically form . submit ()
如果用户没有意识到他们已经登录到攻击者的帐户,他们可能会将个人数据(例如信用卡信息或搜索历史记录)添加到帐户中。然后攻击者可以重新登录他们的账户来查看这些数据。
谷歌过去容易受到登录 CSRF 攻击。
我们可以使用上面提到的双重提交 Cookie 模式来阻止登录 CSRF——攻击者将无法猜测 CSRF 令牌,这意味着他们无法发起 CSRF 登录攻击。
包起来
CSRF 代表跨站点请求伪造。有两种 CSRF 攻击:
- 正常的 CSRF
- 登录 CSRF
在 Normal CSRF 中,攻击者旨在通过请求创建状态更改。
在登录 CSRF 中,攻击者旨在诱骗用户登录攻击者的帐户——如果用户不知道,则希望从用户的操作中受益。
您可以使用 Double Submit Cookie 模式和 Cookie to header 方法来防止这两种 CSRF 攻击。将sameSite
设置为strict
会阻止正常的 CSRF,但不会阻止登录 CSRF。
而已!