某网站用户订单被恶意篡改 安全工程师推荐CSRF漏洞检测工具下载修复
最近有个案例挺典型的,一家电商平台的用户反馈订单金额莫名其妙被改了,本来买的是199元的耳机,结账时却变成了1999元。技术团队排查后发现,问题出在CSRF(跨站请求伪造)漏洞上。一个恶意链接就能在用户无感知的情况下,代替用户提交修改订单的请求。这事儿说大不大,但用户信任一旦崩塌,再想捡回来可就难了。今天咱们就好好聊聊CSRF漏洞是什么、怎么检测、怎么修。
一、CSRF到底是个啥玩意儿
先别被这些专业术语吓到,用大白话说,CSRF就是坏人利用你的身份,替你干坏事。
想象一下这个场景:
你正在登录某购物网站,浏览器里还挂着这个网站的登录状态。这时候你收到一条消息,点开链接看到一张搞笑图片——实际上这个链接背后藏着一个恶意请求,当你点击的瞬间,浏览器会自动带上你的Cookie,替你去那个购物网站提交一个”修改收货地址”的请求,而你压根儿什么都没看见,事情就已经发生了。
这就是CSRF的核心原理:攻击者诱导用户点击恶意链接或访问恶意页面,利用用户已登录的身份,在用户不知情的情况下,向目标网站发送伪造的请求。
听起来有点复杂?再举个生活中的例子:
你在银行柜台办业务,窗口的小姐姐让你填一张表格。你信了,老老实实填了。结果这张表格被坏人偷偷调包了,你以为是转账500块,实际上表格上写的是转账50万。这就是CSRF。
二、这次事故是怎么发生的
回到开头的案例,那个电商平台是怎么被拖下水的?
技术人员复盘了攻击链路,发现流程是这样的:
攻击者构造恶意页面:写了一个网页,里面包含一段自动提交的表单,指向平台的”修改订单”接口,比如:
<form action="https://shop.example.com/api/order/update" method="POST"> <input type="hidden" name="orderId" value="12345" /> <input type="hidden" name="amount" value="1999" /> <input type="submit" value="Submit" /> </form> <script>document.forms[0].submit();</script>诱导用户点击:通过社交工程,把链接发给已登录平台账号的用户,比如”恭喜您中奖,点击查看”之类的。
用户无感触发:用户点开链接,浏览器自动带上Cookie,表单自动提交,订单金额被恶意修改。
完成攻击:用户可能直到付款时才发现问题,但此时钱已经付出去了。
整个过程,用户啥也不知道,就像在自家客厅被人悄无声息地偷了东西。
三、为什么CSRF能这么”为所欲为”
很多技术人员会问:既然用户已登录,网站不是应该验证身份吗?为啥还能被利用?
问题出在,这些网站只验证了用户已登录,却没有验证这个请求是不是用户主动发起的。
从技术角度看,CSRF能成功的根本原因有三个:
原因一:依赖Cookie做身份验证
大部分Web应用在用户登录后,会在浏览器存一个Session Cookie。只要Cookie还在,浏览器就会自动带上它发起请求。CSRF攻击就是利用了这一点——用户点击恶意链接时,浏览器”顺手”把Cookie一起带过去了,服务器收到请求后一看:”哟,这位是老用户啊,通过!”
但服务器完全不知道,这个请求是用户自己点出来的,还是被攻击者利用浏览器自动提交的。
原因二:没有”请求来源”校验
正规的请求应该有一个明确的”发起方”。比如用户在页面上填表单、点按钮,请求里会带上一些标识(比如Token),服务器收到请求后需要验证这个Token是不是自己签发的。
很多网站压根没做这一步。请求来了就处理,完全不看”你是从哪来的”。
原因三:敏感操作没有二次确认
修改订单、转账、改密码这类操作,按理应该让用户再确认一次,比如弹个框问”确定要改吗?”或者输入验证码。但很多网站为了”用户体验流畅”,省略了这一步。结果攻击者一个链接就能替你完成操作。
四、怎么检测CSRF漏洞
检测到CSRF漏洞,其实不需要多高深的工具,核心思路就一个:模拟攻击者,看能不能帮用户”替用户”发请求。
下面介绍几种常用的方法和工具。
方法一:手动构造验证页面
这是最基础也最直接的检测方法。你可以自己写一个小HTML页面,模拟攻击者的行为:
<!DOCTYPE html>
<html>
<head>
<title>CSRF测试页面</title>
</head>
<body>
<h2>CSRF漏洞检测工具</h2>
<p>请确保你已登录目标网站后再点击测试</p>
<form action="https://目标网站.com/api/order/update" method="POST" id="csrfForm">
<input type="hidden" name="orderId" value="测试订单号" />
<input type="hidden" name="amount" value="999" />
<button type="button" onclick="submitForm()">点击测试</button>
</form>
<script>
function submitForm() {
document.getElementById('csrfForm').submit();
alert('请求已发送,请查看目标网站订单是否被修改');
}
</script>
</body>
</html>
把这个页面放到你自己能访问的服务器上,让测试账号登录后点击按钮。如果目标网站的订单真的被改了,说明存在CSRF漏洞。
方法二:使用专业安全测试工具
手动写太麻烦?没关系,有很多成熟的工具可以用。
工具一:Burp Suite
Burp Suite是安全测试领域的”瑞士军刀”,里面有个专门的CSRF检测功能。
使用步骤:
- 配置浏览器代理,让流量经过Burp
- 登录目标网站,正常操作一遍
- 在Burp的Proxy历史中找到关键请求(比如修改订单的请求)
- 右键该请求,选择”Engagement tools” -> “Generate CSRF PoC”
- Burp会自动生成一个包含PoC代码的HTML页面,测试人员可以用它来验证漏洞
工具二:OWASP CSRFTester
OWASP(开放Web应用安全项目)推出的专门检测CSRF的工具,界面友好,适合初学者。
下载地址:https://github.com/OWASP/CSRFTester
使用方式:
- 导入目标请求
- 工具自动生成测试页面
- 在目标网站登录后打开测试页面,观察是否发生预期外的请求
工具三:CSRF PoC Generator(在线工具)
如果不想下载安装,可以直接用在线工具。比如:
- CSRF PoC Generator:
https://cirt.net/CSRFTest - 输入目标URL、请求参数,工具自动帮你生成测试代码
方法三:检查响应头是否包含防护机制
一个好的检测手段是看目标网站有没有主动防护。你可以在浏览器的开发者工具里,查看网站响应头:
Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
重点关注这几个头:
| 响应头 | 作用 | 是否存在 |
|---|---|---|
Set-Cookie 中的 HttpOnly |
防止JS读取Cookie,降低XSS攻击风险 | 检查 |
X-Frame-Options |
防止页面被嵌入iframe,限制部分CSRF攻击 | 检查 |
Content-Security-Policy 的 frame-ancestors |
更精细的iframe控制 | 检查 |
SameSite Cookie属性 |
控制Cookie在跨站请求时是否发送 | 检查 |
如果目标网站连最基本的X-Frame-Options都没设,CSRF风险就非常高。
方法四:对比GET和POST请求
CSRF攻击主要针对改变数据的POST请求,对GET请求影响较小(因为GET不应该有副作用)。
检测时可以做这个实验:
- 找一个会触发数据变更的GET请求(比如”删除订单”),看看不带任何Token能不能执行
- 找一个POST请求,同样不带Token试试
- 如果POST请求不需要任何验证就能执行,那基本可以确认存在CSRF漏洞
五、发现漏洞后怎么修
检测到漏洞只是第一步,真正关键的是怎么修。下面按修复难度从低到高,介绍几种主流方案。
方案一:添加Anti-CSRF Token(最推荐)
这是目前业界最主流的防护方式,原理是:
每次渲染页面时,服务器生成一个随机Token,嵌入到表单中。用户提交表单时,Token必须一起传回去。服务器校验Token是否正确,不正确就拒绝请求。
实现步骤:
后端(以Java Spring为例):
// 1. 生成Token并存入Session
@RequestMapping("/order/add")
public String addOrderPage(HttpSession session, Model model) {
// 生成随机Token
String csrfToken = UUID.randomUUID().toString();
session.setAttribute("csrfToken", csrfToken);
// 把Token传给前端
model.addAttribute("csrfToken", csrfToken);
return "order/add";
}
// 2. 验证Token
@PostMapping("/order/submit")
public String submitOrder(@RequestParam String orderId,
@RequestParam String amount,
@RequestParam String csrfToken,
HttpSession session) {
// 从Session中取出存储的Token
String storedToken = (String) session.getAttribute("csrfToken");
// 校验Token
if (storedToken == null || !storedToken.equals(csrfToken)) {
throw new CsrfTokenException("CSRF Token验证失败");
}
// Token用一次就失效(防止重放攻击)
session.removeAttribute("csrfToken");
// 正常处理订单逻辑...
return "success";
}
前端(HTML表单):
<form action="/order/submit" method="POST">
<input type="hidden" name="orderId" value="12345" />
<input type="hidden" name="amount" value="199" />
<!-- 关键:把Token也带上去 -->
<input type="hidden" name="csrfToken" value="{{ csrfToken }}" />
<button type="submit">提交订单</button>
</form>
这样,攻击者就算构造了恶意页面,也不知道Token是什么,提交的请求自然会被服务器拒绝。
方案二:设置SameSite Cookie属性
如果你用的是较新的浏览器,可以给Cookie加上SameSite属性,告诉浏览器:跨站请求时,不要带这个Cookie。
Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly; SameSite=Lax
SameSite有三个可选值:
| 值 | 含义 | 适用场景 |
|---|---|---|
Strict |
所有跨站请求都不带Cookie | 安全性要求极高 |
Lax |
部分跨站请求(如GET导航)带Cookie,POST跨站不带 | 推荐大多数场景使用 |
None |
所有跨站请求都带Cookie | 需要明确允许跨站携带时(一般不推荐) |
注意: 这种方式需要用户浏览器支持,老版本浏览器可能忽略这个属性。所以不能单独依赖它,要和Token机制配合使用。
方案三:校验Referer头
每个HTTP请求都带有一个Referer头,告诉服务器”我是从哪个页面过来的”。
Referer: https://shop.example.com/order/list
CSRF攻击的请求,Referer通常指向攻击者的域名。我们可以校验Referer,确保请求来自本站:
// 校验Referer
String referer = request.getHeader("Referer");
if (referer == null || !referer.startsWith("https://shop.example.com")) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "非法请求来源");
return;
}
缺点: Referer可以被伪造(某些浏览器或代理情况下),所以它只能作为辅助手段,不能单独依赖。
方案四:敏感操作二次验证
对于高风险操作(改收货地址、转账、改密码),强制要求用户二次确认:
- 弹出确认框
- 输入验证码
- 短信/邮件确认
// 修改收货地址前弹出确认
function confirmAddressChange() {
let newAddress = document.getElementById('newAddress').value;
let verifyCode = prompt('为安全起见,请输入手机验证码:');
if (!verifyCode) {
alert('请输入验证码');
return;
}
// 带验证码一起提交
fetch('/api/address/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({
address: newAddress,
verifyCode: verifyCode
})
});
}
方案五:使用框架自带防护
很多现代框架已经内置了CSRF防护,开发时直接用就行:
Spring Security(Java):
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() // 默认开启CSRF保护
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.formLogin()
.and()
.httpBasic();
}
}
Django(Python):
# Django默认开启CSRF保护,只需在表单里加{% csrf_token %}
# 中间件配置(settings.py)
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware', # 默认已开启
...
]
Laravel(PHP):
// Blade模板中加一行即可
<form method="POST" action="/order">
@csrf
...
</form>
如果你正在用这些框架,记得检查一下配置,别自己”手动关闭”了防护功能。
六、完整修复清单
发现CSRF漏洞后,建议按以下清单逐项排查修复:
- [ ] 所有敏感操作(改密码、转账、改地址、下单)都加了Anti-CSRF Token
- [ ] Token是一次性的,每次请求后失效
- [ ] Cookie设置了
SameSite=Lax或Strict - [ ] 响应头设置了
X-Frame-Options: DENY或SAMEORIGIN - [ ] 响应头设置了
Content-Security-Policy: frame-ancestors 'self' - [ ] 对
Referer头做了校验(作为辅助) - [ ] 高风险操作要求二次验证(验证码/短信)
- [ ] 所有Cookie都设置了
HttpOnly,防止JS读取 - [ ] 定期用Burp Suite或CSRFTester做扫描测试
- [ ] 建立代码审查机制,新增接口必须检查CSRF防护
七、最后说几句
CSRF漏洞说白了,就是网站”过于信任浏览器“——以为Cookie带了就等于用户本人操作。但浏览器就是个工具,它不会思考,攻击者随便诱导一下,就能让它帮自己干活。
作为开发者,防护CSRF其实不复杂,核心就记住一句话:让每个敏感请求都带上只有你和用户才知道的”暗号”(Token),并且验证这个暗号。
最后给个实在的建议:上线前,花半个小时用Burp Suite跑一遍,或者让测试同学用CSRFTester扫一下,比出了事再补救强太多了。安全这事儿,防患于未然永远比亡羊补牢便宜。
