了解 CSRF 攻击

2025-05-28

了解 CSRF 攻击

最近,我在编写《理解异步 JavaScript》时开始研究网络安全——我想确保我的建议是安全的,并且我的建议不会对我的任何学生造成伤害。

不幸的是,安全领域的文章相当难懂。文章中有很多词语会引发人们的恐惧、不确定和怀疑。我读这些文章时会感到恐慌——我担心自己最终会做错事——尽管这些文章的初衷是好的!

很多文章也没有详细讲解CSRF攻击,比如如何设置CSRF攻击以及如何预防CSRF攻击,这让我对学到的知识产生了怀疑。最终我不得不自己摸索。

为了让您更容易理解 CSRF,我尝试撰写了一篇关于 CSRF 攻击的完整(循序渐进)信息的文章。希望本文能帮助您清晰地理解并建立安全的 Web 应用程序所需的信心。

两种 CSRF 攻击

CSRF 攻击有两种:

  1. 普通 CSRF 攻击
  2. 登录 CSRF

我们首先讨论普通 CSRF 攻击,然后讨论登录 CSRF。

什么是 CSRF 攻击

CSRF 攻击是一种诱骗受害者向他们经过身份验证(登录)的网站提交恶意请求(他们不想发出的请求)的攻击。

该请求必须来自另一个网站,因此被称为“跨站”。该请求还会冒充已验证的用户,因此被称为“请求伪造”。

CSRF 攻击是盲目的——这意味着攻击者无法看到受害者提交请求后发生的情况。因此,CSRF 攻击通常以服务器状态变化为目标。

什么是状态变更?基本上,任何修改数据库的操作都属于状态变更。状态变更的示例包括:

  • 更改用户名和密码
  • 向账户汇款
  • 从用户账户发送虚假消息
  • 从用户帐户分享不适当的图片或视频

CSRF 攻击利用了浏览器在每次请求中自动向服务器发送 Cookie 的特性。如果没有任何 CSRF 防护措施,当存在身份验证 Cookie 时,服务器可能会认为请求有效。

身份验证 Cookie 可以是任何内容,只要服务器使用它们来检查用户是否有效即可。它可以是访问令牌,也可以是会话 ID。这取决于服务器如何处理身份验证。

CSRF 攻击生效的先决条件

CSRF 攻击要成功需要四个先决条件。

  1. 任何方法的请求都会发送到服务器。
  2. 用户必须经过身份验证。
  3. 服务器必须在 cookie 中存储身份验证信息。
  4. 服务器没有实现 CSRF 预防技术(下面将讨论)。

CSRF攻击的工作原理

在攻击者发起 CSRF 攻击之前,他们需要找到一个一致的目标请求。他们必须知道该请求的作用。这个请求可以是任何请求——GET、POST、PUT 或 DELETE。任何请求都可以。

一旦他们选择了目标请求,他们就必须生成一个虚假请求来欺骗用户。

最后,他们必须诱骗用户发送请求。大多数情况下,这意味着:

  1. 找到一种在用户不知情的情况下自动发送请求的方法。最常见的方法是通过图像标签和自动提交 JavaScript 表单。
  2. 歪曲链接(或按钮),诱骗用户点击。(又称社会工程学)。

通过 GET 请求发起攻击

仅当服务器允许用户使用 GET 请求更改状态时,才会发生基于 GET 请求的 CSRF 攻击。如果您的 GET 请求是只读的,则无需担心此类 CSRF 攻击。

但是,假设我们的服务器不遵循编程最佳实践,允许通过 GET 请求更改状态。如果他们这样做,他们就会有麻烦——大麻烦。

例如,假设有一家银行允许您使用以下端点进行转账。您只需在 GET 请求中输入accountamount即可向某人汇款。

https://bank.com/transfer?account=Mary&amount=100
Enter fullscreen mode Exit fullscreen mode

攻击者可以生成一个链接,将钱汇入他们的账户。

# Sends 9999 to the Attacker's account
https://bank.com/transfer?account=Attacker&amount=9999
Enter fullscreen mode Exit fullscreen mode

此时,攻击者可以找到一种方法,在用户不知情的情况下自动触发链接。

一种方法是将链接包含在网页或电子邮件中的 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"
/>
Enter fullscreen mode Exit fullscreen mode

另一种方法是歪曲链接的功能。这种方法之所以有效,是因为人们在点击链接之前不会检查链接。如果用户点击了链接,他们就会在不知情的情况下向攻击者发送 GET 请求。

<!-- Fake link that triggers the GET request attack -->
<a href="https://bank.com/transfer?account=Attacker&amount=9999"
  >View my Pictures</a
>
Enter fullscreen mode Exit fullscreen mode

如果用户通过了身份验证,服务器将收到一个身份验证 cookie,使其相信该请求是有效的。如果服务器未使用任何 CSRF 保护机制,那么资金将被发送给攻击者。

GET CSRF 攻击示例:

  • uTorrent 在2008 年遭受了 CSRF 攻击,它允许通过 GET 请求更改状态。
  • Youtube 在2008 年曾存在一个安全漏洞,允许攻击者执行用户几乎所有可能的操作,包括发送消息、添加好友列表等。

点击上面的链接,你就能找到一些真实的 GET 请求示例,它们会引发此类 CSRF 攻击。(别担心,这里没有奇怪的链接😜)。

利用 POST 请求进行 CSRF 攻击

使用 POST 请求的 CSRF 攻击遵循相同的模式——但它们不能通过链接或图像标签发送。它们需要通过表单或 JavaScript 发送。

假设我们有相同的易受攻击的端点,攻击者只需输入accountamount信息即可触发请求。

POST https://bank.com/transfer?account=Attacker&amount=9999
Enter fullscreen mode Exit fullscreen mode

攻击者可以创建一个表单,并向用户隐藏account和 的值。点击此虚假表单的用户将在不知情的情况下发送 POST 请求。amount

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

这种表单还可以通过 JavaScript 自动执行,而无需人们知道——真正的用户甚至不需要点击按钮,但他们已经遇到麻烦了。

<form>...</form>
<script>
  const form = document.querySelector('form')
  form.submit()
</script>
Enter fullscreen mode Exit fullscreen mode

POST CSRF 攻击虽然可怕,但也有方法可以预防。我们将在下面的预防部分讨论相关技巧。

利用 PUT 和 DELETE 请求进行 CSRF 攻击

CSRF 攻击无法PUT通过和请求执行DELETE,因为我们使用的技术不允许它们这样做。

是的。你没看错。

CSRF 攻击无法通过 HTML 表单执行,因为表单不支持PUTDELETE请求。它只支持GETPOST。如果您使用任何其他方法(除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>
Enter fullscreen mode Exit fullscreen mode

因此,您永远无法通过 HTML 执行 CSRF 攻击。

现在说点有意思的:如果 HTML 不允许,人们该如何通过表单发送请求呢?经过一番研究,我发现大多数框架都允许发送PUT参数的请求wPOST_method

<!-- How most frameworks handle PUT requets -->
<form method="post" ...>
  <input type="hidden" name="_method" value="put" />
</form>
Enter fullscreen mode Exit fullscreen mode

您可以PUT通过 JavaScript 执行 CSRF 攻击,但当今浏览器和服务器中的默认预防机制使得这些攻击很难发生——您必须故意放松防御才能使其发生。

原因如下。

要执行PUTCSRF 攻击,您需要发送包含该方法的 Fetch 请求put。您还需要包含该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(/*...*/)
})
Enter fullscreen mode Exit fullscreen mode

由于三个原因,这是行不通的。

首先,由于 CORS 的存在,浏览器不会自动执行此请求。除非——当然——服务器允许任何带有以下标头的请求,从而造成漏洞:

Access-Control-Allow-Origin: *
Enter fullscreen mode Exit fullscreen mode

其次,即使您允许所有来源访问您的服务器,您仍然需要一个Access-Control-Allow-Credentials选项让浏览器向服务器发送 cookie。

Access-Control-Allow-Credentials: true
Enter fullscreen mode Exit fullscreen mode

第三,即使您允许向服务器发送 Cookie,浏览器也只会发送sameSite属性设置为 的Cookie none。(这些 Cookie 也称为第三方 Cookie)。

如果您不知道我所说的第三点,那么您是安全的——如果您将身份验证 cookie 作为第三方 cookie 发送,那么您真的必须是一个恶意的开发人员,想要搞乱您的服务器。

这部分内容非常长,很难理解。我写了另外几篇文章来帮助你理解到底发生了什么——以及为什么让自己暴露在PUTCSRF 攻击之下是如此困难:

简而言之,POST除非你的服务器真的被破坏了,否则你只需要担心 CSRF 攻击。

CSRF预防方法

目前最常见的CSRF预防方法是:

  • 双重提交 Cookie 模式
  • Cookie 到 header 方法

两种方法都遵循相同的公式。

当用户访问你的网站时,你的服务器必须创建一个 CSRF 令牌并将其放入浏览器的 Cookie 中。此类令牌的常用名称如下:

  • CSRF令牌
  • X-SRF-令牌
  • X-XSRF-令牌
  • X-CSRF-TOKEN

使用你喜欢的任何代币名称。它们都可以使用。

重要的是,CSRF Token 必须是随机生成的、加密强度高的字符串。如果您使用 Node,可以使用 生成该字符串crypto

import crypto from 'crypto'

function csrfToken (req, res, next) {
  return crypto.randomBytes(32).toString('base64')
}
Enter fullscreen mode Exit fullscreen mode

如果您使用 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' })
 }
})
Enter fullscreen mode Exit fullscreen mode

如何使用 CSRF 令牌取决于您是否支持双重 cookie 提交模式或 Cookie 到标头方法(或两者)。

双重提交 Cookie 模式

这种模式的名称有点误导 — — 因为它似乎意味着使用“双重提交 Cookie”发送两次 cookie。

这实际上意味着:

  1. 你在 cookie 中发送 CSRF 令牌
  2. 您使用 CSRF 令牌进行呈现<form>- 该令牌将包含在表单的提交中。

(因此双重提交)。

如果您使用 Express,则可以将 CSRF 令牌传递到 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
  })
})
Enter fullscreen mode Exit fullscreen mode

然后您可以CSRF_TOKEN按照如下形式使用:

<form>
  <input type="hidden" name="csrf" value="{{CSRF_TOKEN}}" />
  <!-- ... -->
</form>
Enter fullscreen mode Exit fullscreen mode

然后,服务器可以通过比较两个 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

  // ...
})
Enter fullscreen mode Exit fullscreen mode

Cookie 到 Header 方法

Cookie 到请求头的方法类似,只不过是用 JavaScript 执行的。在这种情况下,CSRF Token 必须同时包含在 Cookie 和请求头中。

在这种情况下,我们需要:

  1. 设置credentialsincludesame-origin包含 cookies
  2. 从中获取 CSRF 令牌document.cookies并将其添加为请求标头。

以下是一个示例请求:

// 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')
 }
})
Enter fullscreen mode Exit fullscreen mode

服务器可以像这样检查 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

  // ...
})
Enter fullscreen mode Exit fullscreen mode

使用图书馆让这一切变得更容易

我向您展示了如何手动创建和测试 CSRF 令牌,因为我想让您了解这个过程。

这个过程已经被解决了很多次,所以我们不应该手动完成(除非你正在学习,就像我在这里所做的那样)。

如果您使用 Express,我建议使用csurf库,因为与我在上面的示例中所展示的相比,它更加强大和灵活。

SameSite Cookie 属性

上述示例中的设置sameSitestrict可确保仅当请求来自同一网站时,CSRF 令牌 cookie 才会发送到服务器。这确保了 CSRF 令牌永远不会泄露到外部页面。

您可以选择(但建议)在设置身份验证 Cookie 时将此sameSite属性设置为strict。这样可以确保不会发生 CSRF 攻击,因为身份验证 Cookie 将不再包含在跨站请求中。

sameSite如果您将其设置strict为身份验证 cookie,是否需要 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>
Enter fullscreen mode Exit fullscreen mode

他们还可以使用 JavaScript 自动触发表单。

const form = document.querySelector('form')

// Sends the request automatically
form.submit()
Enter fullscreen mode Exit fullscreen mode

如果用户没有意识到自己已经登录了攻击者的账户,他们可能会将个人数据(例如信用卡信息或搜索历史记录)添加到该账户中。攻击者随后可以重新登录自己的账户查看这些数据。

谷歌过去曾面临登录 CSRF 攻击

我们可以使用上面提到的双重提交 Cookie 模式来防止登录 CSRF——攻击者将无法猜测 CSRF 令牌,这意味着他们无法发起 CSRF 登录攻击。

总结

CSRF 是跨站点请求伪造的缩写。CSRF 攻击有两种:

  1. 正常的 CSRF
  2. 登录 CSRF

在普通的 CSRF 中,攻击者旨在通过请求创建状态改变。

在登录 CSRF 中,攻击者的目的是诱骗用户登录攻击者的帐户 - 并希望在用户不知情的情况下从用户的操作中获益。

您可以使用双重提交 Cookie 模式和将 Cookie 添加到标头的方法预防这两种 CSRF 攻击。设置sameSite为可strict预防普通 CSRF,但不能预防登录 CSRF。

就是这样!


感谢阅读。本文最初发布在我的博客上。如果您想阅读更多文章来帮助您成为更优秀的前端开发人员,请订阅我的新闻通讯。

文章来源:https://dev.to/zellwk/understanding-csrf-attacks-36ao
PREV
Svelte,为什么这么受关注?Svelte 是什么?Rich Harris 的 Svelte 立场 结论
NEXT
三个有用的 Express 中间件