Web 安全

CSRF (跨站请求伪造)

约 3 分钟阅读

什么是 CSRF (跨站请求伪造)

CSRF (Cross-Site Request Forgery) 是一种攻击手法,使用户的浏览器向已认证的 Web 站点发送非用户本意的请求。攻击者准备一个陷阱网页或邮件,受害者一旦查看,其浏览器就会自动向目标站点发送恶意请求。

浏览器会自动附加与请求目标域名绑定的 Cookie,而不论该请求由哪个页面发起,因此如果受害者处于登录状态,攻击者的请求也会被作为已认证请求处理。密码修改、转账、邮箱地址变更、商品购买等改变状态的操作都会成为攻击目标。

攻击原理与具体示例

CSRF 攻击按以下流程执行。

  1. 受害者登录目标站点 (例如网上银行),会话 Cookie 保存在浏览器中
  2. 受害者查看攻击者准备的陷阱页面 (通过邮件链接、论坛帖子等)
  3. 陷阱页面中嵌入的 HTML 或 JavaScript 从受害者的浏览器向目标站点发送请求
  4. 浏览器自动附加会话 Cookie,目标站点将其作为合法请求处理

例如,如果转账功能的设计是由 POST /transfer 从请求体中接收收款方和金额,攻击者只需在陷阱页面中嵌入自动提交的表单,就能从受害者账户执行转账。若把表单放在屏幕外的 iframe 中并用 JavaScript 提交,受害者甚至不会察觉操作已经发生。

虽然容易与 XSS 混淆,但 CSRF 的根本区别在于它是从受害者的浏览器伪造合法请求,而非在受害者的浏览器中执行攻击者的脚本。

防御措施的实现

CSRF 防御的核心是引入验证请求是否基于合法用户意图的机制。

CSRF 令牌

服务器在显示表单时生成随机令牌并嵌入隐藏字段。表单提交时验证令牌是否匹配,从而拒绝来自外部站点的伪造请求。这种方式称为同步令牌模式 (Synchronizer Token Pattern),令牌对每个会话是唯一的,并且由于同源策略,外部站点的 JavaScript 无法读取它。但如果站点存在 XSS,令牌本身也会被窃取,因此 CSRF 令牌只有与 XSS 对策结合才能发挥作用。如果不希望在服务器端保存令牌,可以采用双重提交 Cookie 模式,把带签名的值同时放入 Cookie 和请求中进行比对。

SameSite Cookie 属性

通过设置 CookieSameSite 属性,可以控制跨站请求是否附加 Cookie。

  • SameSite=Strict:不为来自外部站点的任何请求附加 Cookie。最安全,但通过外部链接访问时也无法保持登录状态
  • SameSite=Lax:仅为顶级导航 (链接点击) 的 GET 请求附加 Cookie。由于不为 POST 请求附加 Cookie,可以防止大多数 CSRF 攻击。但在未指定该属性、由浏览器按默认值应用 Lax 的情况下,规则更为宽松,Cookie 设置后 2 分钟内的 POST 请求仍会附加 Cookie
  • SameSite=None:跨站请求也附加 Cookie。必须与 Secure 属性配合使用

SameSite 属性作为多层防御有效,但单靠它并不构成 CSRF 对策。同站判定以可注册域名为单位,因此 app.example.com 发行的 Cookie 在来自 other.example.com 的请求中也被视为同站,子域名被劫持或共享域名上的第三方内容都会成为绕过途径。此外,与外部服务集成时不得不指定 SameSite=None 的 Cookie,以及不应用该默认值的旧版浏览器和嵌入式浏览器依然存在,因此请务必与令牌验证配合使用。

其他防御措施

  • Origin / Referer 头验证:确认请求来源是否为本站。但由于隐私设置或代理,Referer 可能不会被发送
  • 要求自定义头:由于 HTML 表单无法附加任意请求头,要求 X-Requested-With 这类自定义头即可拦截通过表单发出的伪造请求。跨站的 JavaScript 若要附加此类头部,需要 CORS 预检,但如果在启用 Access-Control-Allow-Credentials 的同时放宽允许的来源,这一防御就不再成立,因此允许的来源应控制在最小范围
  • 安全头的职责划分:CSP 和 X-Frame-Options 并不是阻止 CSRF 本身的机制。CSRF 应通过 Cookie 的附加条件和请求来源验证来防御,而安全头则用于封堵 XSS 和点击劫持这些不同的攻击路径

框架内置的 CSRF 防护

许多服务器端 Web 框架都内置提供 CSRF 防护功能。

  • DjangoCsrfViewMiddleware 默认启用,并验证 {% csrf_token %} 模板标签所嵌入的令牌。在 HTTPS 下还会将 Origin 头与 CSRF_TRUSTED_ORIGINS 进行比对,因此也能应对经由子域名的攻击
  • Ruby on Rails:新创建的应用中 config.action_controller.default_protect_from_forgery 处于启用状态,因此会自动生成和验证 CSRF 令牌。如需显式指定,可写 protect_from_forgery with: :exception
  • Spring Security:对 POST 等非安全 HTTP 方法默认启用 CSRF 保护。使用与 Thymeleaf 或 JSP 的集成时,CsrfToken 会被自动嵌入表单
  • Next.js / SPA:在使用 Cookie 认证的 API 路由中,应结合 SameSite Cookie 与 Origin 头验证。也可考虑通过 WAF 进行额外保护

如果禁用框架的 CSRF 保护,请确认替代措施已可靠实现。在 API 端点排除 CSRF 保护时,前提是切换到 Bearer Token 认证,不使用基于 Cookie 的认证。

常见误解

GET 请求不会发生 CSRF
如果通过 GET 请求实现了状态变更操作 (删除、设置变更等),就会成为 CSRF 的目标。仅通过在 img 标签的 src 属性中设置 URL 就能发送 GET 请求,因此状态变更必须使用 POST/PUT/DELETE 实现。即使 <code>SameSite=Lax</code> 成为默认值,Lax 仍将 GET 视为安全方法而放行,因此通过 GET 变更状态的实现无法依靠浏览器的默认设置得到保护。
使用 HTTPS 就能防止 CSRF
HTTPS 提供通信加密和防篡改功能,但与 CSRF 无关。CSRF 是从合法用户的浏览器发送合法请求的攻击,即使通信被加密,攻击仍然成立。
分享

相关术语

相关文章