CSRF:Cross-Site Request Forgery (跨站请求伪造)

Posted on Mon 02 March 2026 in Tech • 3 min read

Abstract CSRF: Cross-Site Request Forgery (跨站请求伪造)
Authors Walter Fan
Category tech note
Status v1.0
Updated 2026-03-02
License CC-BY-NC-ND 4.0

CSRF:Cross-Site Request Forgery (跨站请求伪造)

一句话版:你以为你在刷网页,攻击者在"借你的身份发指令"。

CSRF 这玩意儿,说白了就是"冒用身份"。

它不需要拿到你的密码,也不需要在你的站点里写脚本。它只需要做到一件事:让你的浏览器,在你已经登录的情况下,帮它发一条请求。

🟢 问题:CSRF 和 XSS 有啥区别?

  • XSS:往你页面里塞脚本,在你家客厅里搞事情
  • CSRF:不进你家,站在门外喊你老妈"把钱转了" (而你妈以为是你吩咐的)

What:CSRF 到底是什么

CSRF (Cross-Site Request Forgery) 的关键点就三个:

  • 跨站:请求是从别的网站发起的
  • 借身份:浏览器会自动带上你的 cookie (或某些自动附带的凭证)
  • 伪造动作:伪造的是"状态改变"的请求,比如转账、改邮箱、改收货地址、绑定设备、加管理员

一句话总结:

只要你的系统用 cookie/session 维持登录态,又允许跨站诱导发起状态变更请求,就有 CSRF 的风险。

🟢 问题:这里的"跨站诱导"到底是怎么诱导?

最常见的套路是"钓鱼页 + 自动提交"。比如你点开一个页面, 它在后台悄悄塞了个表单, 自动 POST 到你的站点。

你可能会问: "跨域不是不让读响应吗?"

对, 这就是 CSRF 阴险的地方: 它不需要读响应, 只需要让动作发生。转账成功了没, 你后面再看账单就知道了。


Why:它为什么能成功

1) 浏览器太"听话"

你只要登录过 A 网站,浏览器就会在访问 A 网站时自动携带 cookie。攻击者利用的就是这个"自动"。

2) 你把"谁发起的请求"和"谁应该被允许"混在了一起

很多接口只检查:

  • "这个请求有没有 session" (有)

却没检查:

  • "这个请求是不是从我们自己的页面发起的" (不知道)
  • "这个动作有没有用户的明确确认" (没有)

3) 你把危险动作做得太方便

如果你的转账接口支持 GET,或者允许 POST /transfer?to=...&amount=...,那就等于给攻击者铺好了路。

🟢 问题:为什么说"GET 不能改状态"?

因为 GET 天生容易被"顺手触发": 浏览器预加载、爬虫抓取、页面里的一个 <img src=...> 都可能把 GET 打出去。

你把改状态藏在 GET 里, 等于把"危险动作"做成了"随便碰一下就会响"的按钮。

🔵 反思:你们有没有这种接口?

  • 改密码/改邮箱/绑定手机号,一次请求就完成
  • 没有二次确认,没有验证码,也没有风控
  • 后端不校验 Origin/Referer,也不要求 CSRF token

有一个就够你喝一壶。


How:防 CSRF,先记住两条硬规则

规则 1:"危险动作"一律要求不可伪造的用户意图

常用办法:

  • CSRF token (同步 token / double submit cookie)
  • 对敏感动作加二次确认 (re-auth、短信/邮箱验证码)

🟢 问题:CSRF token 是啥?为什么它能防?

你可以把它理解成"你家门口的暗号"。

  • 你的页面里会带着这个暗号 (比如写在隐藏字段里, 或放在 header 里)
  • 攻击者的网站拿不到暗号

所以它就算能让浏览器把请求发出去, 也过不了服务端校验。

规则 2:别指望前端,服务端必须兜底

你可以在前端加按钮确认,但服务端仍然要验证:

  • CSRF token
  • 或 Origin/Referer (适合作为辅助, 不能当唯一依赖)

How:你可以落地的四件事 (按性价比排序)

如果你用 cookie 做会话,先把默认值想清楚:

  • SameSite=Lax:大多数场景能挡住"第三方站点自动提交表单"这类 CSRF
  • SameSite=Strict:更严格,但可能影响正常跳转体验
  • SameSite=None; Secure:允许跨站携带 cookie (通常用于 SSO/跨站嵌套),但风险更高

一句话:能不用 None 就别用 None

🟢 问题:SameSite=Lax/Strict/None, 到底怎么选?

  • Lax: 默认首选。大多数站点够用, 兼容性也好。
  • Strict: 更像"严防死守"。安全更强, 但可能把一些正常跳转也拦了 (比如从外部链接点进来后再操作)。
  • None: 你明确需要"跨站也带 cookie"时才用, 并且必须配 Secure。一旦用它, CSRF token 基本就别省了。

2) 对所有状态变更请求启用 CSRF token

token 的意义是:攻击者的网站拿不到这个 token,就算能发请求,也过不了校验。

🟢 问题:"同步 token" 和 "double submit cookie" 是啥区别?

  • "同步 token": token 放在服务器 session 里, 每次请求带上, 服务端对比。
  • "double submit cookie": token 放在 cookie 里, 同时也要在 header/表单里再带一份, 服务端要求两份一致。

你不必死记术语。记住那句人话就够了: 服务端要看到"页面里那份 token", 才算你本人点的。

3) 校验 Origin/Referer (当作安全带)

  • 优先校验 Origin
  • Origin 没有时再看 Referer
  • 校验结果最好做 allowlist (只允许你自己的域名)

🟢 问题:为什么它只能当"辅助"?

因为有些场景 header 可能缺失或被代理改写, 你不能把它当成唯一凭证。但作为"多一道闸", 它很便宜, 也很有效。

4) 把危险动作做"不方便"

听起来反人类,其实是救命:

  • 改绑邮箱/手机号:要求重新登录或 OTP
  • 转账/提现吗:多一步确认 + 风控
  • 删除/重置:增加确认口令 (比如输入 "DELETE")

目的无他:让"误触发"和"被诱导"都没那么容易。


Example:三段你可以直接抄的示例 (Java/Go/Python)

Java (Spring Security):默认开启 CSRF,别手贱关掉

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            // 默认就是启用, 这里只是显式写出来, 让人别误删
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        )
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/**").authenticated()
            .anyRequest().permitAll()
        );
    return http.build();
}

🟢 问题:为什么很多人会翻车?

因为有人为了"接口调试方便",顺手来了句 .csrf(AbstractHttpConfigurer::disable)

这句一旦进了主干,你就等着被 CSRF 教做人。

Go (Gin):用 middleware 校验 CSRF token + Origin

下面是"最小可用"的思路示意:生产里你最好用成熟库 (比如 gorilla/csrf),但原理就这样。

func CSRFMiddleware(allowedOrigin string) gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead {
            c.Next()
            return
        }

        origin := c.GetHeader("Origin")
        if origin != "" && origin != allowedOrigin {
            c.AbortWithStatus(http.StatusForbidden)
            return
        }

        token := c.GetHeader("X-CSRF-Token")
        if token == "" {
            c.AbortWithStatus(http.StatusForbidden)
            return
        }

        // TODO: 校验 token (比如和 session 绑定,或者 double submit cookie)
        c.Next()
    }
}

Python (Django):你只要别绕开它,基本就稳

from django.views.decorators.csrf import csrf_protect
from django.http import JsonResponse

@csrf_protect
def change_email(request):
    if request.method != "POST":
        return JsonResponse({"error": "method not allowed"}, status=405)
    # ... 正常业务逻辑 ...
    return JsonResponse({"ok": True})

如果你用的是 Django/DRF,不要随手加 @csrf_exempt。它就像把门锁拆了,说"我只是临时进出方便"。


Summary:CSRF 的核心,其实是"确认意图"

CSRF 不高深,它盯着的是你系统里最朴素的假设:

"只要 cookie 在, 就是本人操作。"

这句话在互联网时代不成立。

所以你需要把系统改成:

"只要是危险动作,就必须能证明这是用户在你站点里亲自点的。"


明天就能做的 5 件事 (Checklist)

  • [ ] 全局搜 .csrf().disable / csrf_exempt:找到就评估风险,能删就删
  • [ ] 把所有状态变更接口过一遍:只允许 POST/PUT/PATCH/DELETE,禁止 GET 改状态
  • [ ] 统一 cookie 策略:优先 SameSite=Lax; Secure; HttpOnly
  • [ ] 对敏感动作加二次确认:改邮箱/改手机号/转账/提现/删数据
  • [ ] 补两条测试:无 token -> 403,跨域 Origin -> 403

扩展阅读


思维导图

@startmindmap
<style>
mindmapDiagram {
  node { BackgroundColor #FAFAFA }
  :depth(0) { BackgroundColor #FFD700 }
  :depth(1) { BackgroundColor #E3F2FD }
  :depth(2) { BackgroundColor #F5F5F5 }
}
</style>
title CSRF (跨站请求伪造)
* CSRF
** What (是什么)
*** 跨站诱导发请求
*** 浏览器自动带 cookie
*** 伪造状态变更动作
** Why (为什么能成)
*** 浏览器太听话
*** 登录 != 授权/意图
*** 危险动作太方便
** How (怎么防)
*** SameSite cookie 策略
*** CSRF token (核心)
*** Origin/Referer 校验 (辅助)
*** 敏感动作二次确认
** Example (怎么写)
*** Java: Spring Security CSRF
*** Go: middleware + token/origin
*** Python: Django csrf_protect
** Action Checklist
*** 找禁用开关
*** 禁止 GET 改状态
*** 统一 cookie 策略
*** 敏感动作加确认
*** 补 CSRF 测试
@endmindmap

CSRF - 思维导图


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。