跳到内容

跨站请求伪造预防速查表

简介

当恶意网站、电子邮件、博客、即时消息或程序欺骗经过身份验证的用户的网络浏览器在受信任的站点上执行不必要的操作时,就会发生跨站请求伪造 (CSRF) 攻击。如果目标用户已在站点上通过身份验证,则不受保护的目标站点无法区分合法的授权请求和伪造的经过身份验证的请求。

由于浏览器请求会自动包含所有 cookie,包括会话 cookie,因此除非使用适当的授权(这意味着目标站点的质询-响应机制不验证请求者的身份和权限),否则此攻击将奏效。实际上,CSRF 攻击使目标系统通过受害者的浏览器执行攻击者指定的功能,而受害者不知情(通常直到未经授权的操作提交之后)。

然而,成功的 CSRF 攻击只能利用易受攻击的应用程序所暴露的功能和用户的权限。根据用户的凭据,攻击者可以转移资金、更改密码、进行未经授权的购买、提升目标账户的权限,或执行用户被允许的任何操作。

简而言之,应遵循以下原则来防御 CSRF:

重要:请记住,跨站脚本 (XSS) 可以击败所有 CSRF 缓解技术!虽然跨站脚本 (XSS) 漏洞可以绕过 CSRF 防护,但 CSRF 令牌对于依赖 cookie 进行身份验证的 Web 应用程序仍然至关重要。请考虑客户端和身份验证方法,以确定应用程序中 CSRF 保护的最佳方法。

  • 有关如何防止 XSS 缺陷的详细指南,请参阅 OWASP XSS 预防速查表
  • 首先,检查您的框架是否具有内置 CSRF 保护并使用它。
  • 如果框架没有内置 CSRF 保护,请将CSRF 令牌添加到所有状态更改请求(导致站点上操作的请求)并在后端验证它们。
  • 有状态软件应使用同步器令牌模式
  • 无状态软件应使用双重提交 cookie
  • 如果 API 驱动的站点不能使用 <form> 标签,请考虑使用自定义请求头
  • 至少实施纵深防御缓解措施部分中的一项缓解措施。
  • SameSite Cookie 属性可用于会话 cookie,但请注意不要为特定域设置 cookie。此操作会引入安全漏洞,因为该域的所有子域将共享该 cookie,如果子域具有不受您控制的域的 CNAME,则这尤其是一个问题。
  • 考虑为高度敏感的操作实施基于用户交互的保护
  • 考虑使用标准头验证来源
  • 不要将 GET 请求用于状态更改操作。
  • 如果由于任何原因您这样做了,请保护这些资源免受 CSRF 攻击。

基于令牌的缓解措施

同步器令牌模式是缓解 CSRF 最流行和推荐的方法之一。

使用内置或现有 CSRF 实现进行 CSRF 保护

由于同步器令牌防御已内置到许多框架中,因此在构建自定义令牌生成系统之前,请先了解您的框架是否默认提供 CSRF 保护。例如,.NET 可以使用内置保护将令牌添加到易受 CSRF 攻击的资源中。如果您选择使用此保护,.NET 会要求您负责正确的配置(例如密钥管理和令牌管理)。

同步器令牌模式

CSRF 令牌应在服务器端生成,并且应每个用户会话或每个请求只生成一次。由于攻击者利用被盗令牌的时间范围对于每个请求令牌来说是最小的,因此它们比每个会话令牌更安全。然而,使用每个请求令牌可能会导致可用性问题。

例如,浏览器的“返回”按钮功能可能会被每个请求令牌阻碍,因为上一页可能包含不再有效的令牌。在这种情况下,与上一页的交互将导致服务器端发生 CSRF 误报安全事件。如果在令牌初始生成后发生每个会话令牌实现,则该值存储在会话中并用于每个后续请求,直到会话过期。

当客户端发出请求时,服务器端组件必须验证该请求中令牌的存在性和有效性,并将其与用户会话中找到的令牌进行比较。如果请求中未找到该令牌或提供的值与用户会话中的值不匹配,则应拒绝该请求。还应考虑采取其他行动,例如将事件记录为正在进行的潜在 CSRF 攻击。

CSRF 令牌应为:

  • 每个用户会话唯一。
  • 秘密。
  • 不可预测(由安全方法生成的大随机值)。

CSRF 令牌可防止 CSRF,因为没有 CSRF 令牌,攻击者无法向后端服务器创建有效请求。

同步模式中传输 CSRF 令牌

CSRF 令牌可以作为响应负载(例如 HTML 或 JSON 响应)的一部分传输到客户端,然后可以作为表单提交中的隐藏字段或通过 AJAX 请求作为自定义头值或 JSON 负载的一部分传输回服务器。CSRF 令牌不应在同步模式中通过 cookie 传输。CSRF 令牌不得泄露在服务器日志或 URL 中。GET 请求可能会在多个位置泄露 CSRF 令牌,例如浏览器历史记录、日志文件、记录 HTTP 请求第一行的网络实用程序,以及如果受保护的站点链接到外部站点,则会泄露 Referer 头。

例如:

<form action="/transfer.do" method="post">
<input type="hidden" name="CSRFToken" value="OWY4NmQwODE4ODRjN2Q2NTlhMmZlYWEwYzU1YWQwMTVhM2JmNGYxYjJiMGI4MjJjZDE1ZDZMGYwMGEwOA==">
[...]
</form>

由于带有自定义头的请求会自动受同源策略的约束,因此通过 JavaScript 将 CSRF 令牌插入自定义 HTTP 请求头比将 CSRF 令牌添加到隐藏字段表单参数中更安全。

如果服务器上维护 CSRF 令牌的状态存在问题,您可以使用一种称为双重提交 Cookie 模式的替代技术。该技术易于实现且无状态。有多种实现此技术的方法,其中朴素模式是最常用的变体。

双重提交 Cookie 模式最安全的实现是签名双重提交 Cookie,它将令牌与用户的身份验证会话(例如,会话 ID)明确绑定。简单地对令牌进行签名而不进行会话绑定提供的保护很少,并且仍然容易受到 Cookie 注入攻击。始终将 CSRF 令牌明确绑定到会话特定数据。

如果令牌包含敏感信息(如会话 ID 或声明),请务必使用基于哈希的消息认证码 (HMAC) 和服务器端密钥。这可以防止令牌伪造,同时确保完整性。HMAC 在所有情况下都优于简单哈希,因为它能抵御各种密码攻击。对于需要令牌内容保密性的场景,请改用经过身份验证的加密。

使用 HMAC CSRF 令牌

要生成 HMAC CSRF 令牌(带会话相关的用户值),系统必须具有:

  • 一个随每次登录会话而变化的会话相关值。此值应仅在用户身份验证会话的整个持续时间内有效。避免使用静态值,例如用户的电子邮件或 ID,因为它们不安全(1 | 2 | 3)。值得注意的是,过于频繁地更新 CSRF 令牌(例如,每个请求更新一次)是一个误解,认为它会增加实质性安全性,而实际上却损害了用户体验(1)。例如,您可以选择以下会话相关值之一或组合:
    • 服务器端会话 ID(例如 PHPASP.NET)。此值绝不应离开服务器或以明文形式出现在 CSRF 令牌中。
    • JWT 中的随机值(例如 UUID),每次创建 JWT 时都会更改。
  • 一个秘密加密密钥。不要与朴素实现中的随机值混淆。此值用于生成 HMAC 哈希。理想情况下,应按照加密存储页面中所述存储此密钥。
  • 一个用于防碰撞的随机值。生成一个随机值(最好是密码学上的随机值),以确保同一秒内的连续调用不会产生相同的哈希(1)。

CSRF 令牌中是否应包含时间戳以实现过期?

将时间戳作为值包含以指定 CSRF 令牌过期时间是一种常见误解。CSRF 令牌不是访问令牌。它们用于通过会话信息验证会话期间请求的真实性。新会话应生成新令牌(1)。

实现 HMAC CSRF 令牌的伪代码

下面是一个伪代码示例,演示了上述实现步骤:

// Gather the values
secret = getSecretSecurely("CSRF_SECRET") // HMAC secret key
sessionID = session.sessionID // Current authenticated user session
randomValue = cryptographic.randomValue(64) // Cryptographic random value

// Create the CSRF Token
message = sessionID.length + "!" + sessionID + "!" + randomValue.length + "!" + randomValue.toHex() // HMAC message payload
hmac = hmac("SHA256", secret, message) // Generate the HMAC hash
// Add the `randomValue` to the HMAC hash to create the final CSRF token.
// Avoid using the `message` because it contains the sessionID in plain text,
// which the server already stores separately.
csrfToken = hmac.toHex() + "." + randomValue.toHex()

// Store the CSRF Token in a cookie
response.setCookie("csrf_token=" + csrfToken + "; Secure") // Set Cookie without HttpOnly flag

下面是一个伪代码示例,演示了从客户端发送回 CSRF 令牌后的验证过程:

// Get the CSRF token from the request
csrfToken = request.getParameter("csrf_token") // From form field, cookie, or header

// Split the token to get the randomValue
const tokenParts = csrfToken.split(".");
const hmacFromRequest = tokenParts[0];
const randomValue = tokenParts[1];

// Recreate the HMAC with the current session and the randomValue from the request
secret = getSecretSecurely("CSRF_SECRET") // HMAC secret key
sessionID = session.sessionID // Current authenticated user session
message = sessionID.length + "!" + sessionID + "!" + randomValue.length + "!" + randomValue

// Generate the expected HMAC
expectedHmac = hmac("SHA256", secret, message)

// Compare the HMAC from the request with the expected HMAC
if (!constantTimeEquals(hmacFromRequest, expectedHmac)) {
    // HMAC validation failed, reject the request
    response.sendError(403, "Invalid CSRF token")
    logError("Invalid CSRF token", hmacFromRequest, expectedHmac)
    return
}

// CSRF validation passed, continue processing the request
// ...

注意:应使用 constantTimeEquals 函数来比较 HMAC,以防止计时攻击。此函数以恒定时间比较两个字符串,无论匹配的字符数如何。

朴素双重提交 Cookie 方法是一种可扩展且易于实现的技术,它使用密码学上强大的随机值作为 cookie 和请求参数(甚至在用户身份验证之前)。然后服务器验证 cookie 值和请求值是否匹配。站点必须要求用户发出的每个事务请求都包含此随机值作为隐藏表单值或在请求头中。如果服务器端值匹配,则服务器接受其为合法请求;如果不匹配,则拒绝请求。

由于攻击者无法在跨站请求期间访问 cookie 值,因此他们无法在隐藏表单值或作为请求参数/头中包含匹配值。

尽管朴素双重提交 Cookie 方法简单且可扩展,但它仍然容易受到 Cookie 注入攻击,特别是当攻击者控制子域或允许他们植入或覆盖 Cookie 的网络环境时。例如,攻击者控制的子域(例如,通过 DNS 接管)可以注入匹配的 Cookie,从而伪造有效的请求令牌。此资源详细说明了这些漏洞。因此,请始终首选带有会话绑定 HMAC 令牌的签名双重提交 Cookie 模式来缓解这些威胁。

禁止简单请求

当使用 <form> 标签提交数据时,它会发送一个浏览器未指定为“需要预检”的“简单”请求。这些“简单”请求会引入 CSRF 风险,因为浏览器允许它们发送到任何源。如果您的应用程序在客户端的任何地方使用 <form> 标签提交数据,您仍然需要使用本文档中描述的其他方法(例如令牌)来保护它们。

警告: 如果浏览器错误允许自定义 HTTP 头,或不强制对非简单内容类型进行预检,则可能会损害您的安全性。虽然不太可能,但在您的威胁模型中考虑这一点是谨慎的。实施 CSRF 令牌增加了额外的防御层,并使开发人员对应用程序的安全性有更多的控制。

禁止简单内容类型

一个请求若要被视为简单请求,它必须具有以下内容类型之一:application/x-www-form-urlencodedmultipart/form-datatext/plain。许多现代 Web 应用程序使用 JSON API,因此自然需要 CORS,但它们可能接受 text/plain,这将容易受到 CSRF 攻击。因此,一个简单的缓解措施是让服务器或 API 禁止这些简单内容类型。

为 AJAX/API 采用自定义请求头

同步器令牌和双重提交 Cookie 都用于防止表单数据伪造,但它们可能难以实现并降低可用性。许多现代 Web 应用程序不使用 <form> 标签来提交数据。一种特别适用于 AJAX 或 API 端点的用户友好型防御是使用自定义请求头。这种方法不需要令牌。

在此模式中,客户端应用会向需要 CSRF 保护的请求附加一个自定义头。该头可以是任意键值对,只要它不与现有头冲突即可。

X-CSRF-Token: RANDOM-TOKEN-VALUE

许多流行的框架使用标准化的头名称进行 CSRF 保护:

  • X-CSRF-Token - Ruby on Rails、Laravel、Django
  • X-XSRF-Token - AngularJS
  • CSRF-Token - Express.js (csurf 中间件)
  • X-CSRFToken - Django

虽然任何任意的头名称都可以,但使用这些标准名称之一可以提高与现有工具和开发人员期望的兼容性。

处理请求时,API 会检查此头是否存在。如果头不存在,后端会拒绝该请求,认为其是潜在的伪造请求。这种方法有几个优点:

  • 无需更改 UI。
  • 不引入服务器状态来跟踪令牌。

这种防御依赖于 CORS 预检机制,该机制向目标服务器发送 OPTIONS 请求以验证 CORS 合规性。所有现代浏览器都将带有自定义头的请求指定为“需要预检”。当 API 验证自定义头存在时,您就知道如果请求来自浏览器,则它必须已经过预检。

自定义头和 CORS

默认情况下,跨域请求 (CORS) 不设置 Cookie。要在 API 上启用 Cookie,您需要设置 Access-Control-Allow-Credentials=true。如果允许凭据,浏览器将拒绝任何包含 Access-Control-Allow-Origin=* 的响应。为了允许 CORS 请求但防止 CSRF,您需要确保服务器仅通过 Access-Control-Allow-Origin 头允许您明确控制的少数几个选定来源。

例如,您可以配置后端以允许来自 http://www.yoursite.comhttp://mobile.yoursite.com 的带 Cookie 的 CORS,因此唯一可能的预检响应是:

Access-Control-Allow-Origin=http://mobile.yoursite.com
Access-Control-Allow-Credentials=true

Access-Control-Allow-Origin=http://www.yoursite.com
Access-Control-Allow-Credentials=true

一种安全性较低的配置是配置您的后端服务器,使用正则表达式允许来自您站点所有子域的 CORS。如果攻击者能够接管子域(这在云服务中并不少见),您的 CORS 配置将允许他们绕过同源策略并伪造带有您自定义头的请求。

处理客户端 CSRF 攻击 (重要)

客户端 CSRF 是一种新的 CSRF 攻击变体,攻击者通过操纵程序的输入参数来欺骗客户端 JavaScript 代码向易受攻击的目标站点发送伪造的 HTTP 请求。客户端 CSRF 起源于 JavaScript 程序使用攻击者控制的输入(例如 URL)来生成异步 HTTP 请求。

注意:这些 CSRF 变体尤其重要,因为它们可以绕过一些常见的反 CSRF 对策,例如基于令牌的缓解措施SameSite cookie。例如,当使用同步器令牌自定义 HTTP 请求头时,JavaScript 程序会将其包含在异步请求中。此外,Web 浏览器会将 cookie 包含在由 JavaScript 程序发起的同站请求上下文中,从而规避SameSite cookie 策略

客户端与传统 CSRF:在传统 CSRF 模型中,服务器端程序是最脆弱的组件,因为它无法区分传入的已验证请求是有意执行的,这也被称为混淆代理问题。在客户端 CSRF 模型中,最脆弱的组件是客户端 JavaScript 程序,因为攻击者可以通过操纵请求端点和/或其参数来生成任意异步请求。客户端 CSRF 是由于输入验证问题导致的,它重新引入了混淆代理缺陷,即服务器端将再次无法区分请求是故意执行的还是无意执行的。

有关客户端 CSRF 漏洞的更多信息,请参阅本文的第 2 节和第 5 节,SameSite wikiCSRF 章,以及Meta Bug Bounty Program此帖子

客户端 CSRF 示例

以下代码片段演示了一个简单的客户端 CSRF 漏洞示例。

<script type="text/javascript">
    const csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");

    const ajaxLoad = () => {
        // process the URL hash fragment
        const hashFragment = window.location.hash.slice(1);

        // hash fragment should be of the format: /^(get|post);(.*)$/
        // e.g., https://site.com/index/#post;/profile
        if (hashFragment.length > 0 && hashFragment.includes(';')) {
            const params = hashFragment.match(/^(get|post);(.*)$/);

            if (params && params.length) {
                const requestMethod = params[1];
                const requestEndpoint = params[3];

                fetch(requestEndpoint, {
                    method: requestMethod,
                    headers: {
                        'X-CSRF-Token': csrf_token,
                        // [...]
                    },
                    // [...]
                })
                .then(response => { /* [...] */ })
                .catch(error => console.error('Request failed:', error));
            }
        }
    };

    // trigger the async request on page load - better practice is to use event listeners
    window.addEventListener('DOMContentLoaded', ajaxLoad);
</script>

漏洞:在此代码片段中,程序在页面加载时调用 ajaxLoad() 函数,该函数负责加载各种网页元素。该函数读取 URL 哈希片段的值(第 4 行),并从中提取两部分信息(即请求方法和端点)以生成异步 HTTP 请求(第 11-13 行)。漏洞发生在第 15-22 行,当 JavaScript 程序使用 URL 片段获取异步 HTTP 请求的服务器端端点(第 15 行)和请求方法时。然而,这两个输入都可以由网络攻击者控制,他们可以选择自己选择的值,并制作包含攻击负载的恶意 URL。

攻击: 通常,攻击者会与受害者共享恶意 URL(通过鱼叉式网络钓鱼电子邮件等元素),并且由于恶意 URL 看起来来自一个诚实、信誉良好(但易受攻击)的网站,用户通常会点击它。或者,攻击者可以创建一个攻击页面来滥用浏览器 API(例如 window.open() API)并欺骗目标页面的易受攻击的 JavaScript 发送 HTTP 请求,这与传统 CSRF 攻击的攻击模型非常相似。

有关客户端 CSRF 的更多示例,请参阅 Meta Bug Bounty Program此帖子和此 USENIX Security 论文

客户端 CSRF 缓解技术

独立请求:当异步请求无法通过攻击者可控制的输入(例如 URL窗口名称文档引用者postMessages 等)生成时,可以防止客户端 CSRF。

输入验证:根据上下文和功能,实现输入和请求参数之间的完全隔离可能并非总是可行。在这些情况下,必须实施输入验证检查。这些检查应严格评估请求参数值的格式和选择,并决定它们是否只能用于非状态更改操作(例如,只允许 GET 请求和以预定义前缀开头的端点)。

预定义请求数据:另一种缓解技术是在 JavaScript 代码中存储预定义、安全的请求数据列表(例如,端点、请求方法和其他可以安全重放的参数的组合)。然后,程序可以使用 URL 片段中的开关参数来决定每个 JavaScript 函数应该使用列表中的哪个条目。

纵深防御技术

SameSite 是一个 cookie 属性(类似于 HTTPOnly、Secure 等),旨在缓解 CSRF 攻击。它在 RFC6265bis 中定义。此属性帮助浏览器决定是否随跨站请求发送 cookie。此属性的可能值是 LaxStrictNone

“Strict”值将阻止浏览器在所有跨站浏览上下文中将 Cookie 发送到目标站点,即使是点击常规链接。例如,如果一个类似 GitHub 的网站使用“Strict”值,一个已登录的 GitHub 用户尝试点击企业讨论论坛或电子邮件中发布的私人 GitHub 项目链接时,该用户将无法访问该项目,因为 GitHub 将不会收到会话 Cookie。由于银行网站不允许任何事务性页面从外部站点链接,因此“Strict”标志最适合银行。

如果网站希望在用户从外部链接到达后保持其登录会话,则 SameSite 的默认 Lax 值在安全性与可用性之间提供了合理的平衡。如果上述 GitHub 场景使用 Lax 值,则在从外部网站点击常规链接时将允许会话 cookie,同时在 POST 等易受 CSRF 攻击的请求方法中阻止它。只有在 Lax 模式下允许的跨站请求才具有顶级导航并使用安全 HTTP 方法。

有关 SameSite 值的更多详细信息,请查看 rfc 的以下部分

使用此属性的 cookie 示例:

Set-Cookie: JSESSIONID=xxxxx; SameSite=Strict
Set-Cookie: JSESSIONID=xxxxx; SameSite=Lax

所有现代桌面和移动浏览器都支持 SameSite 属性。主要例外是旧版浏览器,包括 Opera Mini(所有版本)、UC 浏览器 Android 版和较旧的移动浏览器(iOS Safari < 13.2、Android 浏览器 < 97)。要跟踪实现此属性的浏览器以及属性的使用方式,请参阅以下服务。Chrome 在 2020 年将 SameSite=Lax 作为默认行为,Firefox 和 Edge 也紧随其后。此外,对于标记为 SameSite=None 的 cookie,需要 Secure 标志。

需要注意的是,此属性应作为纵深防御概念的额外层来实施。此属性通过支持它的浏览器保护用户,并且它还包含两种绕过方式,如以下部分所述。此属性不应取代 CSRF 令牌。相反,它应与该令牌共存,以更健壮的方式保护用户。

使用标准头验证来源

这种缓解方法分为两个步骤,两者都检查 HTTP 请求头值:

  1. 确定请求的来源(源站来源)。可以通过 Origin 或 Referer 头完成。
  2. 确定请求的目标来源。

在服务器端,我们验证两者是否匹配。如果匹配,我们接受请求为合法请求(表示是同源请求),如果不匹配,我们丢弃请求(表示请求源自跨域)。对这些头的可靠性源于它们不能通过编程方式更改,因为它们属于禁用头列表,这意味着只有浏览器可以设置它们。

识别源站来源 (通过 Origin/Referer 头)

检查 Origin 头

如果 Origin 头存在,请验证其值是否与目标来源匹配。与 Referer 不同,Origin 头将存在于源自 HTTPS URL 的 HTTP 请求中。

如果 Origin 头不存在,检查 Referer 头

如果 Origin 头不存在,请验证 Referer 头中的主机名是否与目标来源匹配。这种 CSRF 缓解方法也常用于未经身份验证的请求,例如在建立会话状态(需要跟踪同步令牌)之前发出的请求。

在这两种情况下,请确保目标来源检查严格。例如,如果您的站点是 example.org,请确保 example.org.attacker.com 不会通过您的来源检查(即,在来源后通过尾部 / 进行匹配,以确保您正在匹配整个来源)。

如果这两个头都不存在,您可以选择接受或阻止请求。我们建议阻止。或者,您可能希望记录所有此类实例,监视它们的使用案例/行为,然后在获得足够信心后才开始阻止请求。

识别目标来源

通常,确定目标来源并非总是那么容易。您不能总是简单地从请求的 URL 中获取目标来源(即其主机名和端口#),因为应用程序服务器经常位于一个或多个代理之后。这意味着原始 URL 可能与应用程序服务器实际收到的 URL 不同。但是,如果您的应用程序服务器由其用户直接访问,那么使用 URL 中的来源是正常的,并且您已准备就绪。

如果您位于代理之后,则有许多选项需要考虑。

  • 配置您的应用程序以简单地知道其目标来源: 由于这是您的应用程序,您可以找到其目标来源并在某些服务器配置条目中设置该值。这将是最安全的方法,因为它是在服务器端定义的,因此是一个受信任的值。然而,如果您的应用程序部署在许多地方,例如开发、测试、QA、生产,并且可能还有多个生产实例,这可能会难以维护。为每种情况设置正确的值可能很困难,但如果您可以通过某个集中配置并为您的实例提供从中获取值的能力,那就太棒了!(注意:确保集中配置存储安全维护,因为您的 CSRF 防御的主要部分依赖于它。)
  • 使用 Host 头值: 如果您希望您的应用程序自行查找其目标,而无需为每个部署实例进行配置,我们建议使用 Host 系列头。Host 头旨在包含请求的目标来源。但是,如果您的应用程序服务器位于代理之后,则 Host 头值很可能被代理更改为代理后面的 URL 的目标来源,这与原始 URL 不同。这个修改后的 Host 头来源将与原始 Origin 或 Referer 头中的源来源不匹配。
  • 使用 X-Forwarded-Host 头值:为了避免代理更改主机头的可能性,您可以使用另一个名为 X-Forwarded-Host 的头来包含代理收到的原始 Host 头值。大多数代理会将原始 Host 头值传递到 X-Forwarded-Host 头中。因此,X-Forwarded-Host 中的值很可能是您需要与 Origin 或 Referer 头中的源来源进行比较的目标来源值。

当请求中存在 origin 或 referrer 头时,使用此头值进行缓解将正常工作。尽管这些头在大多数情况下都会包含,但在少数情况下它们不会包含(其中大多数是出于合法原因,以保护用户隐私/适应浏览器生态系统)。

不使用 X-Forward-Host 的用例:

  • 在遵循跨域 302 重定向的实例中,重定向请求中不包含 Origin,因为这可能被视为不应发送到其他源的敏感信息。
  • 在某些隐私敏感上下文中,Origin 设置为“null”。例如,请参阅此处
  • Origin 头包含在所有跨域请求中,但对于同源请求,在大多数浏览器中仅包含在 POST/DELETE/PUT 中。注意:虽然这不理想,但许多开发人员使用 GET 请求执行状态更改操作。
  • Referer 头也不例外。在多种使用场景中,Referer 头也会被省略(12345)。负载均衡器、代理和嵌入式网络设备也因隐私原因而在记录时剥离 Referer 头而闻名。

通常,一小部分流量确实属于上述类别(1-2%),没有任何企业愿意失去这部分流量。互联网上流行的一种使这项技术更易于使用的方法是,如果 Origin/referrer 与您配置的域列表“或”空值匹配,则接受请求(示例此处。空值是为了涵盖上述未发送这些头的边缘情况)。请注意,攻击者可以利用这一点,但由于部署所需的工作量很小,人们更喜欢将此技术用作纵深防御措施。

使用带有主机前缀的 Cookie 识别来源

虽然前面提到的 SameSiteSecure 属性限制了已设置 cookie 的发送,并且 HttpOnly 限制了已设置 cookie 的读取,但攻击者仍可能尝试注入或覆盖受保护的 cookie(参见会话固定攻击)。对带有 CSRF 令牌的 cookie 使用 Cookie Prefixes 也扩展了对这类攻击的安全保护。如果 cookie 具有 __Host- 前缀,例如 Set-Cookie: __Host-token=RANDOM; path=/; Secure,则每个 cookie:

  • 不能被其他子域(覆盖)写入,并且
  • 不能有 Domain 属性。
  • 必须具有 / 路径。
  • 必须标记为 Secure(即,不能通过未加密的 HTTP 发送)。

除了 __Host- 前缀外,浏览器厂商还支持较弱的 __Secure- 前缀。它放宽了对域覆盖的限制,即它们:

  • 可以有 Domain 属性,并且
  • 可以被子域覆盖。
  • 可以有 / 以外的 Path

如果已验证的用户需要访问不同的(子)域,此宽松的变体可以用作“域锁定”__Host- 前缀的替代方案。在所有其他情况下,建议除了 SameSite 属性之外,还使用 __Host- 前缀。

Cookie 前缀受所有主流浏览器支持

有关 cookie 前缀的更多信息,请参阅 Mozilla 开发网络IETF 草案

基于用户交互的 CSRF 防御

虽然这里引用的所有技术都不需要任何用户交互,但有时让用户参与事务以防止未经授权的操作(通过 CSRF 或其他方式伪造)更容易或更合适。以下是一些在正确实施时可以作为强大 CSRF 防御的技术示例。

  • 重新身份验证机制
  • 一次性令牌

不要使用 CAPTCHA,因为它专门设计用于防御机器人。在某些 CAPTCHA 的实现中,可以从不同的用户会话获取人类交互/存在的证明,这仍然是有效的。尽管这使 CSRF 漏洞利用更加复杂,但它并不能防御 CSRF。

虽然这些是非常强大的 CSRF 防御措施,但它们可能会对用户体验产生显著影响。因此,它们通常只用于安全关键操作(例如密码更改、资金转账等),并与本速查表中讨论的其他防御措施一起使用。

登录表单中可能存在的 CSRF 漏洞

大多数开发人员倾向于忽略登录表单上的 CSRF 漏洞,因为他们认为 CSRF 不适用于登录表单,因为用户在该阶段未通过身份验证,但这种假设并非总是正确的。CSRF 漏洞仍然可以在用户未通过身份验证的登录表单上发生,但影响和风险不同。

例如,如果攻击者使用 CSRF 在购物网站上使用攻击者的账户冒充目标受害者的已验证身份,并且受害者随后输入其信用卡信息,则攻击者可能能够使用受害者存储的卡详细信息购买商品。有关登录 CSRF 和其他风险的更多信息,请参阅本文的第 3 节。

登录 CSRF 可以通过创建预会话(用户身份验证之前的会话)并在登录表单中包含令牌来缓解。您可以使用上述任何一种技术生成令牌。请记住,一旦用户通过身份验证,预会话就不能转换为真实会话——会话应该被销毁并创建一个新会话以避免会话固定攻击。这种技术在“跨站请求伪造的鲁棒防御”第 4.1 节中有所描述。登录 CSRF 还可以通过在 AJAX 请求中包含自定义请求头来缓解,如上文所述。

参考:演示 CSRF 保护的 JEE 过滤器示例

以下 JEE Web 过滤器提供了本速查表中描述的一些概念的示例参考。它实现了以下无状态缓解措施(OWASP CSRFGuard 涵盖了有状态方法)。

  • 使用标准头验证同源
  • 双重提交 Cookie
  • SameSite cookie 属性

请注意,这仅是一个参考示例,不完整(例如:它没有在源和引用头检查成功时引导控制流的块,也没有对引用头进行端口/主机/协议级别验证)。建议开发人员在此参考示例的基础上构建完整的缓解措施。开发人员还应在检查 CSRF 之前实施身份验证和授权机制,这被认为是有效的。

完整源代码位于此处,并提供了可运行的 POC。

JavaScript:自动将 CSRF 令牌作为 AJAX 请求头包含

以下 JavaScript 指南默认将 GETHEADOPTIONS 方法视为安全操作。因此,GETHEADOPTIONS 方法的 AJAX 调用无需附加 CSRF 令牌头。但是,如果这些动词用于执行状态更改操作,它们也需要 CSRF 令牌头(尽管这是一种不良做法,应避免)。

POSTPUTPATCHDELETE 方法作为状态更改动词,应将 CSRF 令牌附加到请求中。以下指南将演示如何在 JavaScript 库中创建覆盖,以使上述状态更改方法的所有 AJAX 请求自动包含 CSRF 令牌。

将 CSRF 令牌值存储在 DOM 中

CSRF 令牌可以包含在 <meta> 标签中,如下所示。页面中的所有后续调用都可以从这个 <meta> 标签中提取 CSRF 令牌。它也可以存储在 JavaScript 变量或 DOM 上的任何位置。但是,不建议将 CSRF 令牌存储在 cookie 或浏览器本地存储中。

以下代码片段可用于将 CSRF 令牌作为 <meta> 标签包含:

<meta name="csrf-token" content="{{ csrf_token() }}">

填充 content 属性的确切语法取决于您的 Web 应用程序的后端编程语言。

覆盖默认值以设置自定义头

几个 JavaScript 库允许您覆盖默认设置,以便自动将头添加到所有 AJAX 请求中。

XMLHttpRequest (原生 JavaScript)

XMLHttpRequest 的 open() 方法可以被覆盖,以便在下次调用 open() 方法时设置 X-CSRF-Token 头。下面定义的 csrfSafeMethod() 函数将过滤掉安全的 HTTP 方法,并且只将头添加到不安全的 HTTP 方法中。

这可以通过以下代码片段来实现:

<script type="text/javascript">
    const csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");

    const csrfSafeMethod = (method) => {
        // these HTTP methods do not require CSRF protection
        return /^(GET|HEAD|OPTIONS)$/.test(method);
    };

    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(...args) {
        const result = originalOpen.apply(this, args);

        if (!csrfSafeMethod(args[0])) {
            this.setRequestHeader('X-CSRF-Token', csrf_token);
        }

        return result;
    };
</script>

现代框架中的 CSRF 防护

现代单页应用程序 (SPA) 框架(如 Angular、React 和 Vue)通常依赖于 Cookie-to-Header 模式来缓解跨站请求伪造 (CSRF) 攻击。这种方法利用了这样一个事实:浏览器会自动将 Cookie 附加到跨域请求中,但只有在同源上运行的 JavaScript 才能读取值和设置自定义头——这使得检测和阻止伪造请求成为可能。Cookie-to-Header 模式的工作原理如下:

  1. 服务器生成 CSRF 令牌:当用户身份验证或加载应用程序时,服务器会在 Cookie 中设置一个 CSRF 令牌(例如,XSRF-TOKEN)。这个 Cookie 可以通过 JavaScript 访问(即,不是 HttpOnly),并且通常具有 SameSite=LaxStrict
  2. 客户端读取令牌:SPA(通常使用像 Angular 的 HttpClient 或 React/Vue 中的 axios 这样的库)从 Cookie 中读取 CSRF 令牌。
  3. 客户端将令牌附加到自定义头:对于每个状态更改请求(POSTPUTDELETE 等),客户端将令牌设置为自定义 HTTP 头(通常是 X-XSRF-TOKENX-CSRF-TOKEN)。
  4. 服务器验证令牌:服务器检查头中的令牌是否与 Cookie 中的令牌匹配。如果匹配,则接受请求;如果不匹配,则将其拒绝为潜在的伪造请求。

Angular 开箱即用地提供了这种模式,通过其 HttpClient 自动处理步骤 2 和 3。相比之下,React 和 Vue 等框架要求开发人员手动或使用辅助库(如 axios 拦截器)实现此逻辑。这种模式确保即使浏览器在伪造请求中包含 Cookie,攻击者也无法从其他源设置匹配的自定义头。

Angular

Angular 的 HttpClient 支持用于防止 XSRF 攻击的 Cookie-to-Header 模式。执行 HTTP 请求时,一个拦截器会从 cookie 中读取令牌(默认为 XSRF-TOKEN),并将其设置为 HTTP 头 X-XSRF-TOKEN。更多文档可在 Angular 关于HttpClient XSRF/CSRF 安全的文档中找到。

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withXsrfConfiguration({})),
    provideRouter(routes, withComponentInputBinding()),
  ],
};

此代码片段已在 Angular 版本 19.2.11 中测试。

React

对于 React 应用程序,您可以使用 axios 拦截器来实现 cookie 到头部的模式:

// csrf-protection.js
import axios from 'axios';

// Function to get the CSRF token from cookies
const getCsrfToken = () => {
  const tokenCookie = document.cookie
    .split('; ')
    .find(cookie => cookie.startsWith('XSRF-TOKEN='));

  return tokenCookie ? tokenCookie.split('=')[1] : '';
};

// Create an axios instance with interceptors
const api = axios.create();

// Add a request interceptor to include the CSRF token in headers
api.interceptors.request.use(config => {
  // Only add for state-changing methods
  if (!/^(GET|HEAD|OPTIONS)$/i.test(config.method)) {
    config.headers['X-CSRF-Token'] = getCsrfToken();
  }
  return config;
});

export default api;

Axios

Axios 允许我们为 POST、PUT、DELETE 和 PATCH 操作设置默认头。

<script type="text/javascript">
    const csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");

    // Set CSRF token for state-changing methods
    axios.defaults.headers.post['X-CSRF-Token'] = csrf_token;
    axios.defaults.headers.put['X-CSRF-Token'] = csrf_token;
    axios.defaults.headers.delete['X-CSRF-Token'] = csrf_token;
    axios.defaults.headers.patch['X-CSRF-Token'] = csrf_token;

    // For TRACE method
    axios.defaults.headers.trace = {
        'X-CSRF-Token': csrf_token
    };

    // Alternative: Using interceptors for all requests
    axios.interceptors.request.use(config => {
        // Only add for state-changing methods
        if (!/^(GET|HEAD|OPTIONS)$/i.test(config.method)) {
            config.headers['X-CSRF-Token'] = csrf_token;
        }
        return config;
    });
</script>

此代码片段已在 Axios 1.9.0 版本中测试。

jQuery

JQuery 暴露了一个名为 $.ajaxSetup() 的 API,可用于向 AJAX 请求添加 X-CSRF-Token 头。$.ajaxSetup() 的 API 文档可在此处找到。下面定义的 csrfSafeMethod() 函数将过滤掉安全的 HTTP 方法,并且只将头添加到不安全的 HTTP 方法中。

您可以配置 jQuery 以通过采用以下代码片段自动将令牌添加到所有请求头中。这为您的基于 AJAX 的应用程序提供了简单便捷的 CSRF 保护:

<script type="text/javascript">
    const csrf_token = $('meta[name="csrf-token"]').attr('content');

    const csrfSafeMethod = method => {
        // these HTTP methods do not require CSRF protection
        return /^(GET|HEAD|OPTIONS)$/i.test(method);
    };

    $.ajaxSetup({
        beforeSend: (xhr, settings) => {
            if (!csrfSafeMethod(settings.type) && !settings.crossDomain) {
                xhr.setRequestHeader("X-CSRF-Token", csrf_token);
            }
        }
    });
</script>

此代码片段已在 jQuery 3.7.1 版本中测试。

用于 CSRF 保护的 TypeScript 工具

TypeScript 允许您为 CSRF 保护创建强类型实用工具。这是一个用于 CSRF 令牌管理的通用实用工具模块:

// csrf-protection.ts

/**
 * Configuration options for CSRF protection
 */
interface CSRFOptions {
  /** Cookie name where the CSRF token is stored */
  cookieName: string;
  /** HTTP header name to use when sending the token */
  headerName: string;
  /** HTTP methods that require CSRF protection */
  unsafeMethods: string[];
}

/**
 * Default configuration for CSRF protection
 */
const DEFAULT_CSRF_OPTIONS: CSRFOptions = {
  cookieName: 'XSRF-TOKEN',
  headerName: 'X-CSRF-Token',
  unsafeMethods: ['POST', 'PUT', 'PATCH', 'DELETE']
};

/**
 * CSRF Protection utility class
 */
export class CSRFProtection {
  private options: CSRFOptions;

  constructor(options: Partial<CSRFOptions> = {}) {
    this.options = { ...DEFAULT_CSRF_OPTIONS, ...options };
  }

  /**
   * Extract CSRF token from cookies
   * @returns The CSRF token or empty string if not found
   */
  public getToken(): string {
    const cookieValue = document.cookie
      .split('; ')
      .find(cookie => cookie.startsWith(`${this.options.cookieName}=`));

    return cookieValue ? cookieValue.split('=')[1] : '';
  }

  /**
   * Check if the given HTTP method requires CSRF protection
   */
  public requiresProtection(method: string): boolean {
    return this.options.unsafeMethods.includes(method.toUpperCase());
  }

  /**
   * Add CSRF token to the provided headers object if needed
   */
  public addTokenToHeaders(method: string, headers: Record<string, string>): Record<string, string> {
    if (this.requiresProtection(method)) {
      const token = this.getToken();
      if (token) {
        headers[this.options.headerName] = token;
      }
    }
    return headers;
  }
}

// Usage example:
// const csrfProtection = new CSRFProtection();
// const headers = csrfProtection.addTokenToHeaders('POST', {});

Angular 与 TypeScript

Angular 使用 TypeScript 构建,使其天然适合强类型 CSRF 保护。下面的示例展示了如何使用 TypeScript 配置 Angular 的 CSRF 保护:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';

import { routes } from './app.routes';

// Configure CSRF protection with custom options
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN', // Name of cookie containing token
        headerName: 'X-XSRF-TOKEN' // Header name for token submission
      })
    ),
    provideRouter(routes)
  ]
};

对于处理 CSRF 令牌的自定义 HTTP 拦截器:

// csrf.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class CsrfInterceptor implements HttpInterceptor {
  private readonly TOKEN_HEADER_NAME = 'X-CSRF-Token';
  private readonly SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'];

  constructor() {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    // Skip CSRF protection for safe methods
    if (this.SAFE_METHODS.includes(request.method)) {
      return next.handle(request);
    }

    // Get token from cookie
    const token = this.getTokenFromCookie();

    if (token) {
      // Clone the request and add the CSRF token header
      const modifiedRequest = request.clone({
        headers: request.headers.set(this.TOKEN_HEADER_NAME, token)
      });
      return next.handle(modifiedRequest);
    }

    return next.handle(request);
  }

  private getTokenFromCookie(): string {
    const tokenCookie = document.cookie
      .split('; ')
      .find(cookie => cookie.startsWith('XSRF-TOKEN='));

    return tokenCookie ? tokenCookie.split('=')[1] : '';
  }
}

React 与 TypeScript

以下是使用 axios 的 React 应用程序的 TypeScript 实现:

// csrf-axios.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

/**
 * Create an axios instance with CSRF protection
 */
export function createCSRFProtectedAxios(
  options: {
    baseURL?: string;
    csrfHeaderName?: string;
    csrfCookieName?: string;
  } = {}
): AxiosInstance {
  const {
    baseURL = '',
    csrfHeaderName = 'X-CSRF-Token',
    csrfCookieName = 'XSRF-TOKEN'
  } = options;

  // Create axios instance
  const instance = axios.create({ baseURL });

  // Add CSRF token interceptor
  instance.interceptors.request.use((config: AxiosRequestConfig) => {
    // Only add for non-GET requests
    if (config.method && !['get', 'head', 'options'].includes(config.method.toLowerCase())) {
      const token = getCsrfToken(csrfCookieName);

      if (token && config.headers) {
        config.headers[csrfHeaderName] = token;
      }
    }
    return config;
  });

  return instance;
}

/**
 * Extract CSRF token from cookies
 */
function getCsrfToken(cookieName: string): string {
  const tokenCookie = document.cookie
    .split('; ')
    .find(cookie => cookie.startsWith(`${cookieName}=`));

  return tokenCookie ? tokenCookie.split('=')[1] : '';
}

// USAGE EXAMPLE

// Define api.ts
// import { createCSRFProtectedAxios } from './csrf-axios';
// export const api = createCSRFProtectedAxios({
//   baseURL: '/api',
//   csrfHeaderName: 'X-CSRF-Token'
// });

// In a React component:
// import { api } from './api';
// 
// function UserProfile() {
//   const updateUser = async (userData: UserData) => {
//     try {
//       // CSRF token is automatically added
//       const response = await api.post('/users/profile', userData);
//       return response.data;
//     } catch (error) {
//       console.error('Failed to update profile', error);
//     }
//   };
//   
//   // Rest of component...
// }

对于使用 Fetch API 的 React 应用程序(TypeScript 版):

// csrf-fetch.ts

/**
 * Interface for CSRF protection options
 */
interface CSRFFetchOptions {
  csrfHeaderName: string;
  csrfCookieName: string;
  baseUrl: string;
}

/**
 * A wrapper around fetch API with CSRF protection
 */
export class CSRFProtectedFetch {
  private options: CSRFFetchOptions;

  constructor(options: Partial<CSRFFetchOptions> = {}) {
    this.options = {
      csrfHeaderName: 'X-CSRF-Token',
      csrfCookieName: 'XSRF-TOKEN',
      baseUrl: '',
      ...options
    };
  }

  /**
   * Performs a fetch request with CSRF protection
   */
  public async fetch<T>(
    url: string, 
    options: RequestInit = {}
  ): Promise<T> {
    const { method = 'GET' } = options;
    const fullUrl = `${this.options.baseUrl}${url}`;

    // Create headers with CSRF token for unsafe methods
    const headers = new Headers(options.headers);

    if (!['GET', 'HEAD', 'OPTIONS'].includes(method.toUpperCase())) {
      const token = this.getCsrfToken();
      if (token) {
        headers.append(this.options.csrfHeaderName, token);
      }
    }

    // Perform request
    const response = await fetch(fullUrl, {
      ...options,
      headers
    });

    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    return response.json();
  }

  /**
   * Shorthand for POST requests
   */
  public async post<T>(url: string, data: any, options: RequestInit = {}): Promise<T> {
    return this.fetch<T>(url, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        ...options.headers,
        'Content-Type': 'application/json'
      }
    });
  }

  /**
   * Extract CSRF token from cookies
   */
  private getCsrfToken(): string {
    const tokenCookie = document.cookie
      .split('; ')
      .find(cookie => cookie.startsWith(`${this.options.csrfCookieName}=`));

    return tokenCookie ? tokenCookie.split('=')[1] : '';
  }
}

// USAGE EXAMPLE

// Create an instance
// const api = new CSRFProtectedFetch({
//   baseUrl: '/api',
//   csrfHeaderName: 'X-CSRF-Token'
// });
// 
// // In React component
// const updateUser = async (userData: UserData) => {
//   try {
//     // CSRF token is automatically added
//     return await api.post('/users/profile', userData);
//   } catch (error) {
//     console.error('Failed to update profile', error);
//   }
// };

CSRF