Cross-site request forgery (CSRF)
在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF
漏洞示例,同时说明如何防御 CSRF
攻击。
什么是 CSRF
跨站请求伪造(CSRF
)是一种 web 安全漏洞,它允许攻击者诱使用户执行他们不想执行的操作。攻击者进行 CSRF
能够部分规避同源策略。
CSRF 攻击能造成什么影响
在成功的 CSRF
攻击中,攻击者会使受害用户无意中执行某个操作。例如,这可能是更改他们帐户上的电子邮件地址、更改密码或进行资金转账。根据操作的性质,攻击者可能能够完全控制用户的帐户。如果受害用户在应用程序中具有特权角色,则攻击者可能能够完全控制应用程序的所有数据和功能。
CSRF 是如何工作的
要使 CSRF
攻击成为可能,必须具备三个关键条件:
- 相关的动作。攻击者有理由诱使应用程序中发生某种动作。这可能是特权操作(例如修改其他用户的权限),也可能是针对用户特定数据的任何操作(例如更改用户自己的密码)。
- 基于 Cookie 的会话处理。执行该操作涉及发出一个或多个 HTTP 请求,应用程序仅依赖会话cookie 来标识发出请求的用户。没有其他机制用于跟踪会话或验证用户请求。
- 没有不可预测的请求参数。执行该操作的请求不包含攻击者无法确定或猜测其值的任何参数。例如,当导致用户更改密码时,如果攻击者需要知道现有密码的值,则该功能不会受到攻击。
假设应用程序包含一个允许用户更改其邮箱地址的功能。当用户执行此操作时,会发出如下 HTTP 请求:
POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 30 Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE email=wiener@normal-user.com
这个例子符合 CSRF
要求的条件:
- 更改用户帐户上的邮箱地址的操作会引起攻击者的兴趣。执行此操作后,攻击者通常能够触发密码重置并完全控制用户的帐户。
- 应用程序使用会话 cookie 来标识发出请求的用户。没有其他标记或机制来跟踪用户会话。
- 攻击者可以轻松确定执行操作所需的请求参数的值。
具备这些条件后,攻击者可以构建包含以下 HTML 的网页:
<html> <body> <form action="https://vulnerable-website.com/email/change" method="POST"> <input type="hidden" name="email" value="pwned@evil-user.net" /> </form> <script> document.forms[0].submit(); </script> </body> </html>
如果受害用户访问了攻击者的网页,将发生以下情况:
- 攻击者的页面将触发对易受攻击的网站的 HTTP 请求。
- 如果用户登录到易受攻击的网站,其浏览器将自动在请求中包含其会话 cookie(假设 SameSite cookies 未被使用)。
- 易受攻击的网站将以正常方式处理请求,将其视为受害者用户发出的请求,并更改其电子邮件地址。
注意:虽然 CSRF
通常是根据基于 cookie 的会话处理来描述的,但它也出现在应用程序自动向请求添加一些用户凭据的上下文中,例如 HTTP Basic authentication 基本验证和 certificate-based authentication 基于证书的身份验证。
如何构造 CSRF 攻击
手动创建 CSRF
攻击所需的 HTML 可能很麻烦,尤其是在所需请求包含大量参数的情况下,或者在请求中存在其他异常情况时。构造 CSRF
攻击的最简单方法是使用 Burp Suite Professional
(付费软件) 中的 CSRF PoC generator
。
如何传递 CSRF
跨站请求伪造攻击的传递机制与反射型 XSS 的传递机制基本相同。通常,攻击者会将恶意 HTML 放到他们控制的网站上,然后诱使受害者访问该网站。这可以通过电子邮件或社交媒体消息向用户提供指向网站的链接来实现。或者,如果攻击被放置在一个流行的网站(例如,在用户评论中),则只需等待用户上钩即可。
请注意,一些简单的 CSRF
攻击使用 GET 方法,并且可以通过易受攻击网站上的单个 URL 完全自包含。在这种情况下,攻击者可能不需要使用外部站点,并且可以直接向受害者提供易受攻击域上的恶意 URL 。在前面的示例中,如果可以使用 GET 方法执行更改电子邮件地址的请求,则自包含的攻击如下所示:
<img src="https://vulnerable-website.com/email/change?email=pwned@evil-user.net">
防御 CSRF 攻击
防御 CSRF
攻击最有效的方法就是在相关请求中使用 CSRF token
,此 token
应该是:
- 不可预测的,具有高熵的
- 绑定到用户的会话中
- 在相关操作执行前,严格验证每种情况
可与 CSRF token
一起使用的附加防御措施是 SameSite cookies
。
常见的 CSRF 漏洞
最有趣的 CSRF
漏洞产生是因为对 CSRF token
的验证有问题。
在前面的示例中,假设应用程序在更改用户密码的请求中需要包含一个 CSRF token
:
POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm csrf=WfF1szMUHhiokx9AHFply5L2xAOfjRkE&email=wiener@normal-user.com
这看上去好像可以防御 CSRF
攻击,因为它打破了 CSRF
需要的必要条件:应用程序不再仅仅依赖 cookie 进行会话处理,并且请求也包含攻击者无法确定其值的参数。然而,仍然有多种方法可以破坏防御,这意味着应用程序仍然容易受到 CSRF
的攻击。
CSRF token 的验证依赖于请求方法
某些应用程序在请求使用 POST 方法时正确验证 token ,但在使用 GET 方法时跳过了验证。
在这种情况下,攻击者可以切换到 GET 方法来绕过验证并发起 CSRF
攻击:
GET /email/change?email=pwned@evil-user.net HTTP/1.1 Host: vulnerable-website.com Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm
CSRF token 的验证依赖于 token 是否存在
某些应用程序在 token 存在时正确地验证它,但是如果 token 不存在,则跳过验证。
在这种情况下,攻击者可以删除包含 token 的整个参数,从而绕过验证并发起 CSRF
攻击:
POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 25 Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm email=pwned@evil-user.net
CSRF token 未绑定到用户会话
有些应用程序不验证 token 是否与发出请求的用户属于同一会话。相反,应用程序维护一个已发出的 token 的全局池,并接受该池中出现的任何 token 。
在这种情况下,攻击者可以使用自己的帐户登录到应用程序,获取有效 token ,然后在 CSRF
攻击中使用自己的 token 。
CSRF token 被绑定到非会话 cookie
在上述漏洞的变体中,有些应用程序确实将 CSRF token
绑定到了 cookie,但与用于跟踪会话的同一个 cookie 不绑定。当应用程序使用两个不同的框架时,很容易发生这种情况,一个用于会话处理,另一个用于 CSRF
保护,这两个框架没有集成在一起:
POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=wiener@normal-user.com
这种情况很难利用,但仍然存在漏洞。如果网站包含任何允许攻击者在受害者浏览器中设置 cookie 的行为,则可能发生攻击。攻击者可以使用自己的帐户登录到应用程序,获取有效的 token 和关联的 cookie ,利用 cookie 设置行为将其 cookie 放入受害者的浏览器中,并在 CSRF
攻击中向受害者提供 token 。
注意:cookie 设置行为甚至不必与 CSRF
漏洞存在于同一 Web 应用程序中。如果所控制的 cookie 具有适当的范围,则可以利用同一总体 DNS 域中的任何其他应用程序在目标应用程序中设置 cookie 。例如,staging.demo.normal-website.com
域上的 cookie 设置函数可以放置提交到 secure.normal-website.com
上的 cookie 。
CSRF token 仅要求与 cookie 中的相同
在上述漏洞的进一步变体中,一些应用程序不维护已发出 token 的任何服务端记录,而是在 cookie 和请求参数中复制每个 token 。在验证后续请求时,应用程序只需验证在请求参数中提交的 token 是否与在 cookie 中提交的值匹配。这有时被称为针对 CSRF
的“双重提交”防御,之所以被提倡,是因为它易于实现,并且避免了对任何服务端状态的需要:
POST /email/change HTTP/1.1 Host: vulnerable-website.com Content-Type: application/x-www-form-urlencoded Content-Length: 68 Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa&email=wiener@normal-user.com
在这种情况下,如果网站包含任何 cookie 设置功能,攻击者可以再次执行 CSRF
攻击。在这里,攻击者不需要获得自己的有效 token 。他们只需发明一个 token ,利用 cookie 设置行为将 cookie 放入受害者的浏览器中,并在 CSRF
攻击中向受害者提供此 token 。
基于 Referer 的 CSRF 防御
除了使用 CSRF token
进行防御之外,有些应用程序使用 HTTP Referer
头去防御 CSRF
攻击,通常是验证请求来自应用程序自己的域名。这种方法通常不太有效,而且经常会被绕过。
注意:HTTP Referer
头是一个可选的请求头,它包含链接到所请求资源的网页的 URL 。通常,当用户触发 HTTP 请求时,比如单击链接或提交表单,浏览器会自动添加它。然而存在各种方法,允许链接页面保留或修改 Referer
头的值。这通常是出于隐私考虑。
Referer 的验证依赖于其是否存在
某些应用程序当请求中有 Referer
头时会验证它,但是如果没有的话,则跳过验证。
在这种情况下,攻击者可以精心设计其 CSRF
攻击,使受害用户的浏览器在请求中丢弃 Referer
头。实现这一点有多种方法,但最简单的是在托管 CSRF
攻击的 HTML 页面中使用 META 标记:
<meta name="referrer" content="never">
Referer 的验证可以被规避
某些应用程序以一种可以被绕过的方式验证 Referer
头。例如,如果应用程序只是验证 Referer
是否包含自己的域名,那么攻击者可以将所需的值放在 URL 的其他位置:
http://attacker-website.com/csrf-attack?vulnerable-website.com
如果应用程序验证 Referer
中的域以预期值开头,那么攻击者可以将其作为自己域的子域:
http://vulnerable-website.com.attacker-website.com/csrf-attack
CSRF tokens
在本节中,我们将解释什么是 CSRF token,它们是如何防御的 CSRF 攻击,以及如何生成和验证CSRF token 。
什么是 CSRF token
CSRF token 是一个唯一的、秘密的、不可预测的值,它由服务端应用程序生成,并以这种方式传输到客户端,使得它包含在客户端发出的后续 HTTP 请求中。当发出后续请求时,服务端应用程序将验证请求是否包含预期的 token ,并在 token 丢失或无效时拒绝该请求。
由于攻击者无法确定或预测用户的 CSRF token 的值,因此他们无法构造出一个应用程序验证所需全部参数的请求。所以 CSRF token 可以防止 CSRF 攻击。
CSRF token 应该如何生成
CSRF token 应该包含显著的熵,并且具有很强的不可预测性,其通常与会话令牌具有相同的特性。
您应该使用加密强度伪随机数生成器(PRNG),该生成器附带创建时的时间戳以及静态密码。
如果您需要 PRNG 强度之外的进一步保证,可以通过将其输出与某些特定于用户的熵连接来生成单独的令牌,并对整个结构进行强哈希。这给试图分析令牌的攻击者带来了额外的障碍。
如何传输 CSRF token
CSRF token 应被视为机密,并在其整个生命周期中以安全的方式进行处理。一种通常有效的方法是将令牌传输到使用 POST 方法提交的 HTML 表单的隐藏字段中的客户端。提交表单时,令牌将作为请求参数包含:
<input type="hidden" name="csrf-token" value="CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz" />
为了安全起见,包含 CSRF token 的字段应该尽早放置在 HTML 文档中,最好是在任何非隐藏的输入字段之前,以及在 HTML 中嵌入用户可控制数据的任何位置之前。这可以对抗攻击者使用精心编制的数据操纵 HTML 文档并捕获其部分内容的各种技术。
另一种方法是将令牌放入 URL query 字符串中,这种方法的安全性稍差,因为 query 字符串:
- 记录在客户端和服务器端的各个位置;
- 容易在 HTTP Referer 头中传输给第三方;
- 可以在用户的浏览器中显示在屏幕上。
某些应用程序在自定义请求头中传输 CSRF token 。这进一步防止了攻击者预测或捕获另一个用户的令牌,因为浏览器通常不允许跨域发送自定义头。然而,这种方法将应用程序限制为使用 XHR 发出受 CSRF 保护的请求(与 HTML 表单相反),并且在许多情况下可能被认为过于复杂。
CSRF token 不应在 cookie 中传输。
如何验证 CSRF token
当生成 CSRF token 时,它应该存储在服务器端的用户会话数据中。当接收到需要验证的后续请求时,服务器端应用程序应验证该请求是否包含与存储在用户会话中的值相匹配的令牌。无论请求的HTTP 方法或内容类型如何,都必须执行此验证。如果请求根本不包含任何令牌,则应以与存在无效令牌时相同的方式拒绝请求。
XSS vs CSRF
在本节中,我们将解释 XSS
和 CSRF
之间的区别,并讨论 CSRF token
是否有助于防御 XSS
攻击。
XSS 和 CSRF 之间有啥区别
跨站脚本攻击 XSS
允许攻击者在受害者用户的浏览器中执行任意 JavaScript 。
跨站请求伪造 CSRF
允许攻击者伪造受害用户执行他们不打算执行的操作。
XSS
漏洞的后果通常比 CSRF
漏洞更严重:
CSRF
通常只适用于用户能够执行的操作的子集。通常,许多应用程序都实现CSRF
防御,但是忽略了暴露的一两个操作。相反,成功的XSS
攻击通常可以执行用户能够执行的任何操作,而不管该漏洞是在什么功能中产生的。CSRF
可以被描述为一个“单向”漏洞,因为尽管攻击者可以诱导受害者发出 HTTP 请求,但他们无法从该请求中检索响应。相反,XSS
是“双向”的,因为攻击者注入的脚本可以发出任意请求、读取响应并将数据传输到攻击者选择的外部域。
CSRF token 能否防御 XSS 攻击
一些 XSS
攻击确实可以通过有效使用 CSRF token
来进行防御。假设有一个简单的反射型 XSS
漏洞,其可以被利用如下:
https://insecure-website.com/status?message=<script>/*+Bad+stuff+here...+*/</script>
现在,假设漏洞函数包含一个 CSRF token
:
https://insecure-website.com/status?csrf-token=CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz&message=<script>/*+Bad+stuff+here...+*/</script>
如果服务器正确地验证了 CSRF token
,并拒绝了没有有效令牌的请求,那么该令牌确实可以防止此 XSS 漏洞的利用。这里的关键点是“跨站脚本”的攻击中涉及到了跨站请求,因此通过防止攻击者伪造跨站请求,该应用程序可防止对 XSS
漏洞的轻度攻击。
这里有一些重要的注意事项:
- 如果反射型
XSS
漏洞存在于站点上任何其他不受CSRF token
保护的函数内,则可以以常规方式利用该XSS
漏洞。 - 如果站点上的任何地方都存在可利用的
XSS
漏洞,则可以利用该漏洞使受害用户执行操作,即使这些操作本身受到CSRF token
的保护。在这种情况下,攻击者的脚本可以请求相关页面获取有效的CSRF token
,然后使用该令牌执行受保护的操作。 CSRF token
不保护存储型XSS
漏洞。如果受CSRF token
保护的页面也是存储型XSS
漏洞的输出点,则可以以通常的方式利用该XSS
漏洞,并且当用户访问该页面时,将执行XSS
有效负载。
SameSite cookies
某些网站使用 SameSite cookies 防御 CSRF
攻击。
这个 SameSite
属性可用于控制是否以及如何在跨站请求中提交 cookie 。通过设置会话 cookie 的属性,应用程序可以防止浏览器默认自动向请求添加 cookie 的行为,而不管cookie 来自何处。
这个 SameSite
属性在服务器的 Set-Cookie
响应头中设置,该属性可以设为 Strict
严格或者 Lax
松懈。例如:
SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Strict; SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Lax;
如果 SameSite
属性设置为 Strict
,则浏览器将不会在来自其他站点的任何请求中包含cookie。这是最具防御性的选择,但它可能会损害用户体验,因为如果登录的用户通过第三方链接访问某个站点,那么他们将不会登录,并且需要重新登录,然后才能以正常方式与站点交互。
如果 SameSite
属性设置为 Lax
,则浏览器将在来自另一个站点的请求中包含cookie,但前提是满足以下两个条件:
- 请求使用 GET 方法。使用其他方法(如 POST )的请求将不会包括 cookie 。
- 请求是由用户的顶级导航(如单击链接)产生的。其他请求(如由脚本启动的请求)将不会包括 cookie 。
使用 SameSite
的 Lax
模式确实对 CSRF
攻击提供了部分防御,因为 CSRF
攻击的目标用户操作通常使用 POST 方法实现。这里有两个重要的注意事项:
- 有些应用程序确实使用 GET 请求实现敏感操作。
- 许多应用程序和框架能够容忍不同的 HTTP 方法。在这种情况下,即使应用程序本身设计使用的是 POST 方法,但它实际上也会接受被切换为使用 GET 方法的请求。
出于上述原因,不建议仅依赖 SameSite
Cookie 来抵御 CSRF
攻击。当其与 CSRF token
结合使用时,SameSite
cookies 可以提供额外的防御层,并减轻基于令牌的防御中的任何缺陷。