Skip to content

Commit 5cc6050

Browse files
committed
Optimize CSRF module
1 parent 8e862d4 commit 5cc6050

3 files changed

Lines changed: 108 additions & 13 deletions

File tree

docs/instructions.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,49 @@ XSS的本质是不可信数据进入浏览器页面执行上下文后,被当
9292
| WebSocket XSS | 页面HTML5特性/WebSocket XSS | `<img src=x onerror=alert(1)>` | 服务端广播消息,前端用`innerHTML`写入消息区 |
9393
| postMessage XSS | 页面HTML5特性/PostMessage XSS | `<img src=x onerror=alert(1)>` | 接收窗口未校验origin且用`innerHTML`写入消息 |
9494

95+
## CSRF
96+
97+
当前覆盖基于登录态 Cookie 的状态变更请求、CSRF Token 校验、Origin/Referer 辅助校验三类核心场景。模块重点演示“用户已登录 + 浏览器自动携带凭证 + 服务端缺少请求来源或意图校验”这一条攻击链。
98+
99+
CSRF的本质是攻击者诱导已登录用户访问恶意页面或触发恶意请求,浏览器自动携带目标站点 Cookie/Session 等凭证,服务端误以为请求来自用户本人,从而执行转账、改密、绑定账号等敏感操作。修复优先使用框架内置 CSRF 防护或不可预测的 CSRF Token;Origin/Referer、SameSite Cookie、二次确认、操作审计是重要补充,但不应替代 Token 和服务端鉴权。
100+
101+
已覆盖类型
102+
103+
| 分类 | 已有场景 | 结论 |
104+
| --- | --- | --- |
105+
| 原生漏洞 | `GET /csrf/vul` 只依赖登录态执行转账 | 覆盖最基础的跨站请求伪造风险,GET 状态变更会放大问题 |
106+
| Token防护 | `GET /csrf/safe1` 校验 Session 中的随机 Token | 覆盖 CSRF 的主流修复方式 |
107+
| 来源校验 | `GET /csrf/safe2` 校验 Origin/Referer 的协议、域名、端口 | 适合作为 Token 之外的辅助防线 |
108+
| 安全编码提示 | 页面说明 SameSite、二次确认、短有效期、审计 | 覆盖业务侧加固建议 |
109+
110+
模块覆盖符合综合性靶场定位。后续如需增强,可补充“POST 表单自动提交 PoC 页面”“JSON CSRF/简单请求 Content-Type 限制”“SameSite Cookie 对比”“Origin 缺失策略”“Referer 前缀匹配绕过反例”等专项场景。当前模块保留为基础成因、Token 修复和来源辅助校验主线即可。
111+
112+
### CSRF漏洞场景测试
113+
114+
页面:`/csrf`
115+
116+
| 场景 | 请求 | 测试输入 | 预期结果 |
117+
| --- | --- | --- | --- |
118+
| 页面访问 | `GET /csrf` | 已登录会话 | 页面正常打开,展示原生漏洞、Token防护、Origin/Referer辅助校验和代码片段 |
119+
| 原生转账 | `GET /csrf/vul?receiver=zhangsan&amount=100` | 已登录会话 | 返回当前登录用户、收款人和金额,说明仅凭 Cookie/Session 即可触发状态变更 |
120+
| 未登录访问 | `GET /csrf/vul?receiver=zhangsan&amount=100` | 无登录会话 | 跳转登录页或被认证流程拦截 |
121+
| GET状态变更 | 页面“漏洞场景:原生漏洞场景”表单 | `receiver=zhangsan&amount=100` | 点击后以 GET 打开转账结果,便于观察 CSRF 风险 |
122+
123+
### CSRF安全场景测试
124+
125+
页面:`/csrf`
126+
127+
| 场景 | 请求 | 测试输入 | 预期结果 |
128+
| --- | --- | --- | --- |
129+
| 获取Token | `GET /csrf/getCsrfToken` | 已登录会话 | 返回随机 `csrfToken`,并写入 Session |
130+
| Token缺失 | `GET /csrf/safe1?receiver=zhangsan&amount=100` | 不带 `csrfToken` | 返回 `success=false` 和 “Token失效!” |
131+
| Token错误 | `GET /csrf/safe1?receiver=zhangsan&amount=100&csrfToken=bad` | 错误Token | 返回 `success=false` 和 “Token失效!” |
132+
| Token正确 | `GET /csrf/safe1?receiver=zhangsan&amount=100&csrfToken=<session token>` | Session中生成的Token | 返回当前用户、收款人、金额和Token |
133+
| Origin/Referer缺失 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | 不带来源头 | 返回 `success=false` 和 “Origin/Referer无效!” |
134+
| Origin恶意来源 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | Header `Origin: http://evil.example` | 返回 `success=false` |
135+
| Origin同源 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | Header `Origin: http://127.0.0.1` | 返回当前用户、收款人和金额 |
136+
| Referer同源 | `GET /csrf/safe2?receiver=zhangsan&amount=100` | Header `Referer: http://127.0.0.1/csrf` | 返回当前用户、收款人和金额 |
137+
95138
## SQL注入
96139

97140
当前覆盖JDBC原生拼接、伪预编译拼接、JdbcTemplate拼接、参数化查询、MyBatis动态SQL、Hibernate HQL/原生SQL、JPA JPQL/动态排序等常见开发栈。

src/main/java/top/whgojp/modules/csrf/controller/CsrfController.java

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
import javax.servlet.http.HttpServletRequest;
1414
import javax.servlet.http.HttpServletResponse;
1515
import javax.servlet.http.HttpSession;
16+
import java.net.URI;
17+
import java.net.URISyntaxException;
18+
import java.nio.charset.StandardCharsets;
19+
import java.security.MessageDigest;
1620
import java.util.HashMap;
1721
import java.util.Map;
1822
import java.util.UUID;
@@ -80,12 +84,12 @@ public Map<String, Object> getCsrfToken(HttpSession session, Model model) {
8084

8185
@GetMapping("/safe1")
8286
@ResponseBody
83-
public Map<String, Object> safe1(@RequestParam("receiver") String receiver,@RequestParam("amount") String amount,@AuthenticationPrincipal UserDetails userDetails,@RequestParam("csrfToken") String csrfToken,HttpSession session) {
87+
public Map<String, Object> safe1(@RequestParam("receiver") String receiver, @RequestParam("amount") String amount, @AuthenticationPrincipal UserDetails userDetails, @RequestParam(value = "csrfToken", required = false) String csrfToken, HttpSession session) {
8488
String currentUser = userDetails.getUsername();
8589

8690
String sessionToken = (String) session.getAttribute("csrfToken");
8791
Map<String, Object> result = new HashMap<>();
88-
if (!csrfToken.equals(sessionToken)) {
92+
if (!constantTimeEquals(csrfToken, sessionToken)) {
8993
result.put("success", false);
9094
result.put("message", "Token失效!");
9195
return result;
@@ -102,10 +106,13 @@ public Map<String, Object> safe1(@RequestParam("receiver") String receiver,@Requ
102106
public Map<String, Object> safe2(HttpServletRequest request, @RequestParam("receiver") String receiver, @RequestParam("amount") String amount, @AuthenticationPrincipal UserDetails userDetails, HttpSession session) {
103107
String currentUser = userDetails.getUsername();
104108
Map<String, Object> result = new HashMap<>();
105-
String referer = request.getHeader("referer");
106-
if (referer == null || !referer.startsWith("http://127.0.0.1")) {
109+
String originOrReferer = request.getHeader("Origin");
110+
if (originOrReferer == null) {
111+
originOrReferer = request.getHeader("Referer");
112+
}
113+
if (!isTrustedSameOrigin(request, originOrReferer)) {
107114
result.put("success", false);
108-
result.put("message", "referer无效!");
115+
result.put("message", "Origin/Referer无效!");
109116
return result;
110117
}
111118
result.put("currentUser", currentUser);
@@ -114,4 +121,40 @@ public Map<String, Object> safe2(HttpServletRequest request, @RequestParam("rece
114121
return result;
115122
}
116123

124+
private boolean constantTimeEquals(String requestToken, String sessionToken) {
125+
if (requestToken == null || sessionToken == null) {
126+
return false;
127+
}
128+
return MessageDigest.isEqual(
129+
requestToken.getBytes(StandardCharsets.UTF_8),
130+
sessionToken.getBytes(StandardCharsets.UTF_8)
131+
);
132+
}
133+
134+
private boolean isTrustedSameOrigin(HttpServletRequest request, String originOrReferer) {
135+
if (originOrReferer == null) {
136+
return false;
137+
}
138+
try {
139+
URI uri = new URI(originOrReferer);
140+
String expectedScheme = request.getScheme();
141+
String expectedHost = request.getServerName();
142+
int expectedPort = request.getServerPort();
143+
int actualPort = uri.getPort() == -1 ? defaultPort(uri.getScheme()) : uri.getPort();
144+
145+
return expectedScheme.equalsIgnoreCase(uri.getScheme())
146+
&& expectedHost.equalsIgnoreCase(uri.getHost())
147+
&& expectedPort == actualPort;
148+
} catch (URISyntaxException e) {
149+
return false;
150+
}
151+
}
152+
153+
private int defaultPort(String scheme) {
154+
if ("https".equalsIgnoreCase(scheme)) {
155+
return 443;
156+
}
157+
return 80;
158+
}
159+
117160
}

src/main/resources/templates/vul/csrf/csrf.html

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
<blockquote class="layui-elem-quote layui-quote-nm"
1515
style="font-size: 15px;background-color: #a7deefab;box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075) !important">
1616
<p>
17-
<pre> CSRF:通过诱导用户点击恶意链接或者访问包含攻击代码的页面,利用用户的已登录身份认证信息(如Cookie),在用户不知情的情况下伪造请求,导致执行恶意操作(转账、修改密码)</pre>
17+
<pre> CSRF(跨站请求伪造):攻击者诱导已登录用户访问恶意页面或触发恶意请求,浏览器会自动携带目标站点 Cookie 等凭证,导致服务端误以为请求来自用户本人,从而执行转账、改密、绑定账号等敏感操作。</pre>
18+
<pre> 触发条件:目标操作依赖 Cookie/Session 自动认证,且缺少不可预测的 CSRF Token、Origin/Referer 校验、SameSite Cookie 或二次确认等防护。GET 请求执行状态变更会显著放大风险。</pre>
1819
</p>
1920
</blockquote>
2021
</fieldset>
@@ -54,9 +55,10 @@ <h1><span class="iconfont icon-bug"> 漏洞场景:原生漏洞场景</span></
5455
<div class="layui-card">
5556
<div class="layui-card-header"><i class="fa fa-bullhorn icon-tip"></i>tips</div>
5657
<div class="layui-card-body layui-text layadmin-text">
57-
<pre style="color: #28333e;font-size: 15px;">安全编码规范:
58-
1.添加csrf-token:客户端每次请求操作,服务端都会将请求token与session存储token做校验
59-
2.校验Referer头:通过检测HTTP请求的Referer字段是否属于本站域名</pre>
58+
<pre style="color: #28333e;font-size: 15px;">问题点:
59+
1、转账这类状态变更操作不应使用 GET 请求;本实验保留 GET 便于构造演示。
60+
2、接口只依赖登录态 Cookie,没有校验 CSRF Token、Origin/Referer 或二次确认。
61+
3、当前项目为漏洞演示关闭了 Spring Security CSRF,全局关闭会扩大风险。</pre>
6062
</div>
6163
</div>
6264

@@ -113,7 +115,11 @@ <h1><span class="iconfont icon-anquan"> 安全场景:CSRF Token防护</span><
113115
<div class="layui-card">
114116
<div class="layui-card-header"><i class="fa fa-bullhorn icon-tip"></i>tips</div>
115117
<div class="layui-card-body layui-text layadmin-text">
116-
<pre style="color: #28333e;font-size: 15px;"> CSRF Token(跨站请求伪造令牌)机制的基本原理是在每个敏感请求中附带一个唯一的、随机生成的令牌(Token),服务器通过验证令牌的合法性来确认请求的来源是否可信。</pre>
118+
<pre style="color: #28333e;font-size: 15px;">安全编码建议:
119+
1、真实业务中敏感操作使用 POST/PUT/DELETE 等非 GET 方法,并开启框架内置 CSRF 防护。
120+
2、每个敏感请求携带不可预测的 CSRF Token,服务端与 Session 中的 Token 做校验。
121+
3、Token 不应出现在可被第三方直接构造的固定 URL 中,真实业务中通常放在表单隐藏字段或自定义请求头。
122+
4、配合 SameSite Cookie、二次确认、短有效期和操作审计提升防护强度。</pre>
117123
</div>
118124
</div>
119125

@@ -137,7 +143,7 @@ <h1><span class="iconfont icon-code"> 安全代码</span></h1>
137143
<div class="layui-col-md12" style="margin-top: 10px">
138144
<div class="layui-row layui-col-space15">
139145
<div class="layui-col-md6">
140-
<h1><span class="iconfont icon-anquan"> 安全场景:Referer检测</span></h1>
146+
<h1><span class="iconfont icon-anquan"> 辅助场景:Origin/Referer检测</span></h1>
141147
<div class="layui-tab layui-tab-brief">
142148
<ul class="layui-tab-title">
143149
<li class="layui-this">转账操作</li>
@@ -169,7 +175,10 @@ <h1><span class="iconfont icon-anquan"> 安全场景:Referer检测</span></h1
169175
<div class="layui-card">
170176
<div class="layui-card-header"><i class="fa fa-bullhorn icon-tip"></i>tips</div>
171177
<div class="layui-card-body layui-text layadmin-text">
172-
<pre style="color: #28333e;font-size: 15px;"> 通过检查请求的Referer或Origin头部,服务器可以判断该请求是否来自可信的站点</pre>
178+
<pre style="color: #28333e;font-size: 15px;">辅助检测说明:
179+
Origin/Referer 可以作为 CSRF Token 之外的补充校验,但不应作为唯一防线。
180+
校验时应解析 URL 后精确比较协议、域名和端口,不要使用 startsWith 这类字符串前缀判断。
181+
某些隐私策略、跳转链路或客户端环境可能缺失 Referer,因此缺失时应拒绝敏感操作或走二次确认。</pre>
173182
</div>
174183
</div>
175184

@@ -180,7 +189,7 @@ <h1><span class="iconfont icon-anquan"> 安全场景:Referer检测</span></h1
180189
</div>
181190

182191
<div class="layui-col-md6">
183-
<h1><span class="iconfont icon-code"> 安全代码</span></h1>
192+
<h1><span class="iconfont icon-code"> 辅助代码</span></h1>
184193
<div class="m-auto div-shadow shadow p-3 mb-5 bg-white rounded">
185194
<div class="code-editor" id="safeCsrfReferer">
186195
</div>

0 commit comments

Comments
 (0)