前几天有个朋友在群里吐槽,说他们公司的后台管理系统被人“薅”了,用户数据泄露了一大堆。技术人员查了半天,最后发现罪魁祸首竟然是一个看似无害的“导出Excel”功能按钮。
这故事听着挺离谱对吧?但仔细一想,这其实就是典型的跨站请求伪造(CSRF)攻击。攻击者不需要破解你的密码,也不需要你的服务器有什么漏洞,他们只需要让你“自愿”地点一下按钮,就能以你的身份去干坏事。
今天我们就来聊聊这个话题。我会用大白话给你讲清楚CSRF是怎么回事,为什么AJAX调用如果不去做权限校验就这么容易被钻空子,然后我们还会用真实的代码案例来演示怎么防范它。准备好了吗?咱们开始。
一、什么是CSRF?别被术语吓跑
首先,咱们得把CSRF这个术语拆开了揉碎了理解。
CSRF的全称是Cross-Site Request Forgery,中文叫跨站请求伪造。听着挺复杂,但咱们用一个生活中的例子来类比。
假设你是某银行的VIP客户,你经常登录银行APP查账、转账。有一天,你登录了银行网站,然后在浏览器里开着这个标签页,同时你去逛了其他网站。就在那个看似无害的网站里,藏着这么一段代码:
<img src="https://bank.example.com/transfer?to=attacker_account&amount=10000" />
你以为这只是个图片,对吧?但你不知道的是,这段代码会以你当前登录的身份,悄悄向银行服务器发送一个转账请求。因为浏览器会自动带上你的Cookie,银行服务器一看:“咦,这位VIP客户要转账1万块到这个账户?”然后,它就执行了。
等你反应过来,钱已经没了。
这就是CSRF的核心思路:攻击者利用你已登录的身份,伪造请求,让你不知不觉中完成某个操作。
二、为什么AJAX调用特别容易中招?
你可能会问:“那我不用GET请求,我用POST请求不行吗?我用AJAX异步调用也不行吗?”
还真不行。AJAX调用本身并不能防止CSRF。只要浏览器自动携带Cookie,攻击者就能通过隐藏的表单或者脚本,让你的浏览器以GET或POST的方式向目标网站发送请求。
咱们来看一个更贴近实际开发的例子。假设你有一个导出用户数据的接口:
// 这是你的前端代码,用户点击“导出Excel”按钮
function exportData() {
fetch('/api/export/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
}
这段代码看起来很正常对吧?用户点击按钮,发起一个POST请求,导出数据。但问题在于,如果这个接口没有CSRF防护,攻击者就可以构造一个恶意页面:
<!-- 攻击者网站上的隐藏代码 -->
<form id="csrf-form" action="https://your-site.com/api/export/users" method="POST">
<input type="hidden" name="" value="" />
</form>
<script>
document.getElementById('csrf-form').submit();
</script>
用户只要访问了这个页面,并且正好登录了你的系统,他的浏览器就会悄悄向你的网站发送POST请求,导出所有用户数据。
你看,这就是AJAX调用没做权限校验的后果。攻击者不需要知道你的密码,也不需要破解你的系统,他们只需要让你“中招”。
三、CSRF攻击的真实危害
你可能会觉得:“这种攻击离我很远吧?”
其实不然。CSRF攻击在现实中非常常见,危害也极大。
1. 资金损失
在金融系统中,CSRF攻击可能导致用户账户被转账、资金被窃取。
2. 数据泄露
在企业管理系统中,CSRF攻击可能导致敏感数据被导出、泄露。
3. 账号被篡改
在社交网络或邮箱系统中,CSRF攻击可能导致用户密码被修改、账号被接管。
4. 恶意操作
在后台管理系统中,CSRF攻击可能导致管理员执行危险操作,比如删除数据、关闭系统。
这些危害都不是小概率事件。据OWASP(开放式Web应用程序安全项目)统计,CSRF一直是Web安全十大风险之一。
四、如何防御CSRF攻击?
既然CSRF这么危险,咱们该怎么防御它呢?常见的防御方法有以下几种:
1. 验证Referer头
这是最简单的方法。服务器可以检查请求的Referer头,判断请求是否来自同源。
# Python Flask示例
@app.route('/api/export/users', methods=['POST'])
def export_users():
referer = request.headers.get('Referer')
if referer and 'your-site.com' in referer:
# 执行导出操作
pass
else:
abort(403)
但这种方法有个缺点:Referer头可以被伪造,而且在一些情况下(比如用户从外部链接跳转过来),Referer可能为null。所以这种方法不能作为唯一的防御手段。
2. 使用CSRF Token
这是目前最主流的防御方法。原理是:服务器在生成页面时,给每个用户生成一个唯一的随机Token,并嵌入到表单或请求参数中。服务器在处理请求时,验证这个Token是否合法。
<!-- 服务器生成的HTML页面 -->
<form action="/api/export/users" method="POST">
<input type="hidden" name="csrf_token" value="随机生成的Token" />
<button type="submit">导出Excel</button>
</form>
# Python Flask示例
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
@app.route('/api/export/users', methods=['POST'])
def export_users():
# CSRFProtect会自动验证Token
# 执行导出操作
pass
这种方法非常有效,因为攻击者无法知道这个Token是什么。即使他们构造了恶意页面,也无法获取到合法的Token。
3. 使用SameSite Cookie
这是一种比较新的防御方法。浏览器可以在设置Cookie时,指定SameSite属性,限制Cookie只能在同源请求中发送。
Set-Cookie: session_id=abc123; SameSite=Strict
- SameSite=Strict:Cookie只能在同源请求中发送,完全禁止跨站携带。
- SameSite=Lax:Cookie在顶级导航(比如点击链接跳转)时可以发送,但在其他跨站请求中禁止发送。
这种方法的好处是不需要修改代码,只需要配置服务器即可。但缺点是需要浏览器支持,而且对于GET请求,SameSite=Strict可能会影响用户体验。
4. 双重验证
对于敏感操作,比如转账、修改密码,可以采用双重验证。比如,用户执行敏感操作时,需要输入密码或验证码。
<!-- 修改密码表单 -->
<form action="/api/change-password" method="POST">
<input type="password" name="new_password" placeholder="新密码" />
<input type="password" name="confirm_password" placeholder="确认新密码" />
<input type="password" name="current_password" placeholder="当前密码" />
<button type="submit">修改密码</button>
</form>
这种方法虽然增加了用户的操作步骤,但能显著提高安全性。
五、实战案例:如何为AJAX接口添加CSRF防护
光说不练假把式。咱们来看一个完整的实战案例,看看如何为AJAX接口添加CSRF防护。
假设你有一个后台管理系统,用户登录后可以导出用户数据。你的系统使用了Express作为后端框架,前端使用了Vue.js。
1. 后端:生成并验证CSRF Token
首先,你需要在后端生成一个CSRF Token,并将其发送给前端。同时,后端需要验证这个Token。
// server.js
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
const csrfProtection = csrf({ cookie: true });
app.use(cookieParser());
app.use(csrfProtection);
// 获取CSRF Token
app.get('/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// 导出用户数据接口
app.post('/api/export/users', csrfProtection, (req, res) => {
// 这里可以添加其他权限校验逻辑
res.json({ success: true, data: [...] });
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
这里我们使用了csurf库来生成和验证CSRF Token。csrfProtection中间件会在每个请求中自动验证Token。
2. 前端:获取并使用CSRF Token
前端需要从后端获取CSRF Token,并将其包含在AJAX请求中。
// main.js
import axios from 'axios';
// 获取CSRF Token
axios.get('/csrf-token').then(response => {
window.CSRF_TOKEN = response.data.csrfToken;
});
// 导出用户数据
function exportData() {
axios.post('/api/export/users', {}, {
headers: {
'X-CSRF-Token': window.CSRF_TOKEN
}
}).then(response => {
console.log('导出成功', response.data);
}).catch(error => {
console.error('导出失败', error);
});
}
这里我们使用axios库来发送AJAX请求,并将CSRF Token放在请求头中。
3. 测试:验证CSRF防护是否生效
现在,你可以测试一下CSRF防护是否生效了。你可以构造一个恶意页面,尝试模拟CSRF攻击:
<!-- attacker.html -->
<!DOCTYPE html>
<html>
<body>
<h1>点击我</h1>
<form action="https://your-site.com/api/export/users" method="POST">
<input type="hidden" name="" value="" />
<button type="submit">点击我</button>
</form>
<script>
document.querySelector('form').submit();
</script>
</body>
</html>
当你打开这个页面时,你应该会收到一个403错误,说明CSRF防护生效了。
六、除了CSRF,还要考虑什么?
虽然CSRF防护很重要,但安全是一个多维度的问题。除了CSRF,你还需要考虑以下安全问题:
1. 权限校验
即使你防止了CSRF攻击,也不代表你的系统是安全的。你还需要考虑权限校验的问题。比如,普通用户不应该能够导出所有用户数据。
// server.js
app.post('/api/export/users', csrfProtection, authenticate, authorize(['admin']), (req, res) => {
// 只有管理员才能导出用户数据
res.json({ success: true, data: [...] });
});
这里我们使用了authenticate和authorize中间件来验证用户身份和权限。
2. 输入验证
用户的输入可能包含恶意代码。你需要对输入进行验证和过滤,防止XSS攻击。
// server.js
const validator = require('validator');
app.post('/api/export/users', csrfProtection, authenticate, authorize(['admin']), (req, res) => {
const format = req.body.format;
if (!validator.isIn(format, ['xlsx', 'csv'])) {
return res.status(400).json({ error: 'Invalid format' });
}
// 执行导出操作
res.json({ success: true, data: [...] });
});
3. 日志和监控
你需要记录所有的敏感操作,并监控异常行为。这样,即使发生了安全事件,你也能够及时发现和响应。
// server.js
const logger = require('winston');
app.post('/api/export/users', csrfProtection, authenticate, authorize(['admin']), (req, res) => {
logger.info(`User ${req.user.id} exported users data with format ${req.body.format}`);
// 执行导出操作
res.json({ success: true, data: [...] });
});
七、总结
CSRF攻击是一种常见且危险的安全威胁,尤其在AJAX调用中,如果不去做权限校验和CSRF防护,就很容易被攻击者钻空子。
为了防止CSRF攻击,你可以采用以下方法:
- 验证Referer头
- 使用CSRF Token
- 使用SameSite Cookie
- 双重验证
同时,你还需要考虑权限校验、输入验证、日志和监控等其他安全问题,才能构建一个安全的系统。
希望这篇文章能帮助你理解CSRF攻击和防护方法。如果你觉得有用的话,不妨分享给更多的朋友,让他们也能了解这些安全知识。
好了,今天的分享就到这里。如果你对这个问题还有什么疑问,欢迎在评论区留言,我们一起讨论。