csrf-testing

star 72

检测 CSRF(跨站请求伪造)风险;当目标存在状态变更操作(密码修改、数据删除、转账、设置变更)时触发。

Q16G By Q16G schedule Updated 6/7/2026

name: csrf-testing description: 检测 CSRF(跨站请求伪造)风险;当目标存在状态变更操作(密码修改、数据删除、转账、设置变更)时触发。 when-to-use: 当目标存在状态变更操作(密码修改、数据删除、转账、设置变更)且依赖 Cookie 自动鉴权时 allowed-tools: bash,read_file,list_files,rg user-invocable: false

CSRF 跨站请求伪造检测

成因引用

跨站请求(受害者已登录,浏览器自动携带 Cookie)→ 状态变更端点(服务端依赖 Cookie 自动鉴权且不验证请求来源 / 不要求 CSRF token / SameSite 防护缺失)= CSRF 跨站请求伪造。一句话讲清成因:状态变更操作只检查"是否已登录"而不检查"操作是否由本站发起",攻击者借受害者会话从外部站点发出请求即可触发越权写操作。业务命名不可作筛选——不能用"这是设置接口/这是支付接口"作为是否检测的依据;CSRF 关注的是所有改变服务端状态的端点(POST / PUT / DELETE / PATCH,以及触发状态变更的 GET),按 sink 语义(是否依赖 Cookie 自动鉴权 + 是否缺乏来源验证)判断,不按业务命名筛选。详见同根目录 pentest/web-security-testing/SKILL.md 漏洞成因图谱 · CSRF 行(不在本 skill 重复成因)。

触发线索(基线检查项)

以下是已知的常见 CSRF 触发线索,作为基线起点而非必检硬清单:

  • 适用且已完成 → 标注 [x] done
  • 明确不适用 → 标注 [-] n/a (原因)
  • 基线未列出但实际发现 → 新增条目并标注 [+] added (来源)

基线触发线索按"sink 语义"分类(不按业务命名):

  • Cookie 自动鉴权 sink:状态变更端点鉴权完全依赖 Cookie(无 Authorization 头、无自定义头要求);登录响应设置的 Cookie 属性中缺 SameSite 或为 SameSite=None
  • 写操作端点 sink:POST / PUT / PATCH / DELETE 端点改密码、改邮箱、改 2FA、改地址、转账、删除资源、改权限、改账户配置;上传/删除文件;管理后台批量操作。
  • GET 副作用 sink:GET 方法实际触发状态变更(如 /api/delete?id=/logout?confirm=1/follow?user=、邮件订阅退订链接);这种最容易被忽略,但 CSRF 通过 <img src> 即可触发。
  • token 校验弱 sink:CSRF token 存在但服务端不校验、为空值仍接受、来自其他用户的 token 仍接受、token 全局共用而非 per-session、token 出现在 URL query 易被 Referer 泄露。
  • Referer/Origin 校验弱 sink:移除 Referer 头后请求仍接受;Origin 为空仍接受;子域 evil.target.com 仍接受。
  • 代码模式:后端代码出现 @PostMapping / app.post(...) 中处理状态变更但缺少 CSRF middleware;Spring 关闭 csrf().disable();Django @csrf_exempt;Express 未引入 csurf

思考检查点

加载本 skill 时按这些问题思考:

  • 这个端点是状态变更吗?包括"看起来是 GET 但实际有副作用"的形态。
  • 鉴权方式是什么?纯 Cookie 自动携带 → 直接 CSRF 候选;Authorization 头 → 浏览器跨站不自动携带 → 默认不易 CSRF(但 SSR 场景仍要确认)。
  • 服务端的来源验证是哪一层?CSRF token / SameSite / Referer / Origin / 自定义头?每一层都要独立验证而非"看到一种就推全套"。
  • SameSite=Lax 是否足够?Lax 允许顶层导航 GET 请求和某些 POST 形态;同站不同端口(http://app.com:8080 vs http://app.com:8081)、同站不同子域 (evil.app.com vs target.app.com)在某些浏览器和配置下不被视为跨站。
  • 跨端点不能推全:同一项目里"改密码"有 CSRF token、"改邮箱"可能没有;必须按端点独立验证。

前置条件与安全边界

  • 仅在授权环境测试。
  • 单接口最多 8 次请求。
  • 不构造面向真实用户的钓鱼页面,仅在测试环境验证。
  • CSRF PoC 仅用于证明概念,不实际发送给目标用户。
  • 跨站触发的状态变更若为删除/覆盖/不可逆动作,按 common/closure-verification.md 的《破坏性 / 不可逆动作的闭环边界》执行——回读证明改走哨兵自证或非破坏差分,二者都做不到就停 suspected禁止对真实业务数据执行破坏动作来强行闭环。

检测步骤

Step 1:防护机制识别

检查目标是否具备 CSRF 防护:

  1. CSRF Token:表单中是否有隐藏 token 字段(csrf_token_tokenauthenticity_token__RequestVerificationToken);请求头中是否要求 X-CSRF-Token
  2. SameSite CookieSet-Cookie 响应头是否包含 SameSite=Strict / SameSite=Lax / SameSite=None
  3. Referer/Origin 校验:服务端是否检查请求来源。
  4. 自定义 Header:是否要求 X-Requested-With: XMLHttpRequest 等浏览器跨站不自动携带的自定义头。
  5. Content-Type 约束:是否仅接受 application/json(浏览器表单跨站发不出 JSON 加预检 CORS)。

Step 2:防护绕过测试

防护机制 绕过尝试
CSRF Token 移除 token 参数、使用空值、使用另一用户的 token、使用过期 token、token 仅校验存在不校验值
SameSite=Lax 顶层导航 GET 触发;同站不同端口 / 同站不同子域 POST 触发;form 表单中 method 为 GET 时 Lax 不拦截
Referer 校验 移除 Referer (<meta name="referrer" content="no-referrer">);Referer 中含 target 域名子串绕过
Origin 校验 子域 Origin、空 Origin、null Origin
自定义 Header 简单请求绕过(form-data / text/plain 不触发预检 CORS)

Step 3:PoC 构造

构造最小 CSRF PoC 页面:

<form action="TARGET_URL" method="POST">
  <input type="hidden" name="param1" value="value1">
  <input type="submit" value="Submit">
</form>
<script>document.forms[0].submit();</script>

对 GET 副作用端点用 <img src="TARGET_URL"> 即可触发。

Step 4:闭环验证

  1. 用受害者账户登录浏览器。
  2. 在同一浏览器中打开 PoC 页面(来源为攻击者域名,与目标不同源)。
  3. 确认状态变更是否实际生效(回读验证)。

不可逆动作例外:状态变更若为删除/转账/不可逆动作,不得在受害者真实数据上执行;改用受害者名下哨兵数据或非破坏差分证明,按 common/closure-verification.md《破坏性 / 不可逆动作的闭环边界》,做不到则降 suspected

示例库

正例形态(代码层根因)

  • Spring http.csrf().disable() + Cookie 鉴权的 /api/user/email POST — 经典 token 缺失(cookie-auth-state-change-no-token
  • /api/delete?id=123 用 GET 且只看登录态 — GET 副作用 + 图片标签即可触发(get-side-effect-csrf
  • 服务端把 SameSite=Lax 当万能防护,未额外验证;evil.target.com:8080app.target.com:443 发 POST 仍被处理(samesite-lax-cross-port-bypass
  • Referer 校验代码 if ("target.com" in request.headers.get("Referer", ""))Referer: http://attacker.com/target.com.html 即可绕过(referer-check-empty-bypass

窄化反例(必须避免)

以下是 CSRF 维度的典型窄化误判:

  • "已测一个端点有 CSRF token → 推断全站状态变更端点都有" — 错。CSRF 防护通常在路由级别声明,开发者经常对部分端点 @csrf_exempt 或忘记加 middleware;必须按端点独立验证。
  • "SameSite=Lax 设置了 → 安全" — 错。Lax 允许顶层导航 GET 触发;同站不同端口、同站不同子域在部分场景不被视为跨站;XS-Leaks 与子域接管可能间接绕过。
  • "已测 Cookie 鉴权端点 → 跳过 Authorization 头鉴权端点" — 错。两类是不同鉴权 sink 形态;Authorization 头默认 CSRF 不易触发,但 SSR/GraphQL/前端注入 token 模式下仍可能存在跨站触发路径,需独立验证。
  • "看起来是 GET 不变更状态 → 跳过" — 错。需追踪 GET 是否实际触发状态变更(删除、关注、退订、确认);GET 副作用 + 图片标签是最易隐藏的 CSRF 形态。
  • "Content-Type 是 application/json → 跨站发不出来" — 错。text/plain + body 拼成 JSON 字符串、Flash / XHR 旧绕过、CORS 配置过宽都可能突破;不能只看 Content-Type 推断。

反例义务(必须遵守)

为什么这里是「必须」:反例义务属于交付契约——"该子系统无 CSRF"或"已防护"结论是覆盖完整性的产物声明,缺失反向验证清单会让下游误信"该维度全站安全"。

写"未发现 CSRF"或"已防护"前,产物必须包含:

  • 测过的状态变更候选端点完整清单(按 sink 语义枚举,按业务命名筛选;含改密码/改邮箱/改 2FA/改地址、转账/支付、删除、上传、管理后台、GET 副作用端点)
  • 每个端点测过的形态:token 缺失/空值/复用、SameSite 绕过(同站不同端口/子域)、Referer/Origin 移除、自定义头去除、Content-Type 切换
  • 每个端点的浏览器实测证据(PoC 是否真触发状态变更,回读结果)

清单不完整 → 结论降级为 partial-coverage 并显式声明未覆盖范围。

特别警示:最容易漏的是"GET 副作用端点"和"已测 cookie 鉴权端点就跳过 Authorization 鉴权端点"。前者用 <img> 即可触发;后者在前端把 token 写入 cookie 再回填 Authorization 的模式下仍可能存在跨站路径。

闭环验证要求(必须遵守)

通用闭环口径见同根目录 common/closure-verification.md(技能表 path 列同一抽取根下,需要时 read_file 读取)。核心:完整证据链才判 confirmed,中间信号最多 suspected。本漏洞特有要点:

  • 仅凭"没有 CSRF Token"不得直接判 confirmed,必须确认状态变更通过跨站请求实际生效(回读)。
  • SameSite Cookie 可能已阻止跨站请求,需在攻击者域真实发起跨站请求验证。
  • 破坏性 / 不可逆动作的回读改走哨兵数据或非破坏差分,不得对受害者真实数据真执行。

判定标准

现象 判定
跨站请求成功触发状态变更,通过回读确认生效 confirmed
缺少 CSRF Token 但有 SameSite Cookie 或其他防护,未实际验证 suspected
CSRF Token 有效、SameSite=Strict、且绕过尝试均失败且覆盖完整 not vulnerable
覆盖清单不完整(如未测 GET 副作用 / 未测同站不同端口 SameSite 绕过 / 未跨子系统) partial-coverage

不可逆动作例外:上表 confirmed 行的状态变更若为删除/转账/不可逆动作,按 common/closure-verification.md《破坏性 / 不可逆动作的闭环边界》——回读改用哨兵数据或非破坏差分,不得对受害者真实数据真执行,做不到则降 suspected

修复建议

  • 对所有状态变更操作添加 CSRF Token(Synchronizer Token Pattern 或 Double Submit Cookie),服务端严格校验值,per-session 生成。
  • Cookie 设置 SameSite=Strict(敏感操作)或 SameSite=Lax(普通操作);明确禁止 SameSite=None 除非搭配额外 CSRF token。
  • 验证 OriginReferer 头,按域名结构解析,不接受空值。
  • 对敏感操作要求二次验证(密码确认、验证码、二次 OTP)。
  • 杜绝 GET 触发状态变更;所有写操作收敛到 POST/PUT/PATCH/DELETE,并接受同等 CSRF 防护。
Install via CLI
npx skills add https://github.com/Q16G/aster --skill csrf-testing
Repository Details
star Stars 72
call_split Forks 6
navigation Branch main
article Path SKILL.md
More from Creator