各位朋友,大家好!今天咱们来聊聊一个听起来有点吓人,但其实挺好理解的话题:JavaScript的CSRF攻击,以及如何用Token和同源策略来保护咱们的网站。
开场白:别怕,CSRF没那么神秘
首先,别被CSRF这个缩写吓到,它全称是Cross-Site Request Forgery,翻译过来就是“跨站请求伪造”。 听起来高大上,但本质就是“冒名顶替”。 想象一下,有人偷偷拿着你的身份证去干坏事,这就是CSRF攻击的简化版。 咱们今天要做的,就是给你的身份证加上防伪标记,让坏人没法冒充你。
第一部分:CSRF攻击原理:谁在冒充你?
CSRF攻击的核心在于“冒充”。 攻击者利用你已经登录的身份,在你不知情的情况下,发起一些恶意请求。 这听起来有点抽象,咱们举个实际的例子:
假设你登录了一个银行网站,可以进行转账操作。 转账的URL可能是这样的:
https://bank.example.com/transfer?account=target_account&amount=1000
这个URL的意思是,从你的账户转账1000元到target_account
这个账户。
现在,如果攻击者在另一个网站(比如一个论坛或者恶意广告)上放了一个图片链接:
<img src="https://bank.example.com/transfer?account=attacker_account&amount=10000&csrf_token=invalid" width="0" height="0">
或者,更隐蔽一点,用JavaScript动态创建一个iframe
,然后设置src
属性:
(function(){
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'https://bank.example.com/transfer?account=attacker_account&amount=10000&csrf_token=invalid';
document.body.appendChild(iframe);
setTimeout(function() {
document.body.removeChild(iframe);
}, 100);
})();
如果此时你正好登录了bank.example.com
,并且浏览器保存了你的登录Cookie,那么当你访问这个包含恶意代码的网站时,浏览器会自动把你的Cookie带上,一起发送到bank.example.com
。
银行服务器一看,Cookie没问题,确实是你的登录信息,就会执行这个转账操作,把10000元转到攻击者的账户。
总结一下CSRF攻击的步骤:
- 用户登录受信任网站A。
- 用户在A网站生成Cookie。
- 用户访问恶意网站B。
- B网站向A网站发起请求(通常是GET或POST),浏览器自动带上A网站的Cookie。
- A网站验证Cookie,认为是用户发起的请求,执行操作。
一个表格让你更清晰:
步骤 | 描述 | 攻击者角色 | 用户角色 |
---|---|---|---|
1 | 用户登录受信任网站A | 无 | 登录网站A |
2 | 用户在A网站生成Cookie | 无 | 生成Cookie |
3 | 用户访问恶意网站B | 部署恶意网站B | 访问网站B |
4 | B网站向A网站发起请求(通常是GET或POST),浏览器自动带上A网站的Cookie | 发起请求 | 无 |
5 | A网站验证Cookie,认为是用户发起的请求,执行操作 | 期望执行操作 | 无 |
第二部分:Token防御:给你的请求加个“暗号”
Token是防止CSRF攻击最常用的方法之一。 它的原理是,在每个需要保护的请求中,都加上一个只有服务器和客户端知道的“暗号”,服务器在收到请求时,验证这个“暗号”是否正确,如果正确,才执行操作。
具体实现步骤:
-
服务器生成Token: 在用户登录成功后,服务器生成一个随机的Token,可以是一个UUID或者一个随机字符串。 这个Token要足够复杂,难以被猜测。
function generateToken() { // 使用crypto库生成随机字符串,在Node.js环境下 const crypto = require('crypto'); return crypto.randomBytes(64).toString('hex'); } // 或者,使用UUID库 // const uuid = require('uuid'); // return uuid.v4(); // 示例:将Token存储在Session中 app.post('/login', (req, res) => { // ... 验证用户名和密码 ... req.session.csrfToken = generateToken(); res.send({ message: '登录成功', csrfToken: req.session.csrfToken }); });
-
服务器将Token传递给客户端: Token可以通过多种方式传递给客户端,比如:
- 隐藏的表单字段: 在HTML表单中添加一个隐藏的
<input>
字段,将Token的值放在这个字段里。 - Cookie: 将Token的值放在一个Cookie中。 但是要注意Cookie的
HttpOnly
和Secure
属性,防止XSS攻击和中间人攻击。 - HTTP Header: 在HTTP Header中添加一个自定义的Header,将Token的值放在这个Header里。 这是最安全的方式,因为攻击者无法通过JavaScript读取HTTP Header。
<!-- 隐藏的表单字段 --> <form action="/transfer" method="post"> <input type="hidden" name="csrf_token" value="${csrfToken}"> <input type="text" name="account"> <input type="text" name="amount"> <button type="submit">转账</button> </form> <!-- JavaScript设置HTTP Header --> fetch('/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken // 假设csrfToken存储在JavaScript变量中 }, body: JSON.stringify({ account: 'target_account', amount: 1000 }) }).then(response => { // ... });
- 隐藏的表单字段: 在HTML表单中添加一个隐藏的
-
客户端在发送请求时,携带Token: 客户端在发送需要保护的请求时,必须将Token的值一起发送给服务器。 具体的方式取决于Token的传递方式。
-
服务器验证Token: 服务器在收到请求后,首先验证请求中携带的Token是否和服务器存储的Token一致。 如果一致,才执行操作;否则,拒绝请求。
// 示例:验证Token app.post('/transfer', (req, res) => { const csrfToken = req.body.csrf_token || req.headers['x-csrf-token']; // 从请求体或Header中获取Token if (!csrfToken || csrfToken !== req.session.csrfToken) { return res.status(403).send({ message: 'CSRF攻击 detected!' }); } // ... 执行转账操作 ... res.send({ message: '转账成功' }); });
Token防御的优势:
- 简单易懂,容易实现。
- 可以有效防止CSRF攻击。
Token防御的注意事项:
- Token必须足够复杂,难以被猜测。
- Token必须存储在服务器端,不能只存储在客户端。
- Token必须在每个需要保护的请求中都进行验证。
- Token应该设置过期时间,防止Token泄露后被长期利用。
- 使用HTTPS,防止Token在传输过程中被窃取。
第三部分:同源策略:浏览器自带的“防火墙”
同源策略(Same-Origin Policy,SOP)是浏览器提供的一种安全机制,用于限制来自不同源的脚本对当前文档或从当前文档加载的资源进行访问。 简单来说,同源策略就是“门当户对”,只有来自相同源的脚本才能互相访问。
什么是“源”?
“源”由协议、域名和端口组成。 只有当两个页面的协议、域名和端口都相同时,才被认为是同源。
URL | 协议 | 域名 | 端口 | 同源? |
---|---|---|---|---|
http://www.example.com/page1.html |
http |
www.example.com |
80 |
(与自身) |
https://www.example.com/page2.html |
https |
www.example.com |
443 |
不同源 (协议不同) |
http://www.example.com:8080/page3.html |
http |
www.example.com |
8080 |
不同源 (端口不同) |
http://example.com/page4.html |
http |
example.com |
80 |
不同源 (域名不同) |
同源策略的作用:
同源策略主要限制以下三种行为:
- Cookie、LocalStorage 和 IndexDB 访问: 来自不同源的页面不能互相访问Cookie、LocalStorage 和 IndexDB。
- DOM 访问: 来自不同源的页面不能互相访问DOM。
- XMLHttpRequest (XHR) 请求: 来自不同源的页面不能发起XMLHttpRequest请求。
同源策略如何防御CSRF:
同源策略可以阻止恶意网站通过JavaScript发起跨域请求。 例如,如果你的银行网站设置了正确的Cookie属性(HttpOnly
、Secure
、SameSite
),那么恶意网站就无法通过JavaScript读取你的Cookie,也无法通过JavaScript发起跨域的POST请求。
但是,同源策略并非万能的。 它有一些例外情况:
- 跨域资源共享(CORS): 服务器可以通过设置HTTP Header,允许特定的域名进行跨域访问。 这是一种安全的方式,允许跨域请求,但必须经过服务器的明确授权。
- JSONP: JSONP是一种利用
<script>
标签的跨域请求方法。 由于<script>
标签不受同源策略的限制,因此可以用来获取来自不同源的数据。 但是JSONP只支持GET请求,并且存在安全风险,不建议使用。 - 表单提交: 表单提交不受同源策略的限制。 这就是为什么CSRF攻击可以通过表单提交来发起。 但是,服务器可以通过验证Referer Header来防御这种攻击。
Referer Header:
Referer Header是HTTP请求Header中的一个字段,用于指示请求的来源页面。 服务器可以通过验证Referer Header,判断请求是否来自受信任的域名。
但是,Referer Header并非总是可靠的。 用户可以通过设置浏览器选项,禁用Referer Header。 而且,Referer Header也可能被篡改。
第四部分:结合Token和同源策略,打造坚固的防线
单独使用Token或者同源策略,都存在一定的局限性。 最好的方法是将两者结合起来,打造一个更坚固的防线。
推荐方案:
- 使用Token: 在每个需要保护的请求中,都加上一个Token。
- 设置Cookie属性: 设置Cookie的
HttpOnly
、Secure
和SameSite
属性。HttpOnly
:防止JavaScript读取Cookie。Secure
:只允许通过HTTPS协议传输Cookie。SameSite
:限制Cookie只能在同源请求中发送。SameSite
属性有三个值:Strict
:最严格的限制,Cookie只能在同源请求中发送。Lax
:允许在导航到目标URL的GET请求中发送Cookie(例如,点击链接)。None
:取消所有限制,Cookie可以在任何请求中发送。 但是,如果设置SameSite=None
,必须同时设置Secure
属性。
- 验证Referer Header(可选): 验证Referer Header,作为额外的安全措施。
代码示例(结合Token和SameSite):
// 服务器端
app.post('/login', (req, res) => {
// ... 验证用户名和密码 ...
const csrfToken = generateToken();
req.session.csrfToken = csrfToken;
// 设置Cookie
res.cookie('csrfToken', csrfToken, {
httpOnly: true,
secure: true, // 只有HTTPS可用
sameSite: 'Strict' // 最严格的限制
});
res.send({ message: '登录成功', csrfToken: csrfToken });
});
app.post('/transfer', (req, res) => {
const csrfToken = req.body.csrf_token || req.headers['x-csrf-token'] || req.cookies.csrfToken; // 从请求体、Header或Cookie中获取Token
if (!csrfToken || csrfToken !== req.session.csrfToken) {
return res.status(403).send({ message: 'CSRF攻击 detected!' });
}
// ... 执行转账操作 ...
res.send({ message: '转账成功' });
});
// 客户端
fetch('/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.cookie.replace(/(?:(?:^|.*;s*)csrfTokens*=s*([^;]*).*$)|^.*$/, "$1") // 从Cookie中获取Token
},
body: JSON.stringify({ account: 'target_account', amount: 1000 })
}).then(response => {
// ...
});
第五部分:总结与实战建议
CSRF攻击虽然听起来可怕,但只要我们掌握了它的原理,并采取正确的防御措施,就可以有效地保护我们的网站。
总结一下今天的内容:
- CSRF攻击是一种“冒名顶替”攻击,攻击者利用用户已经登录的身份,发起恶意请求。
- Token是一种常用的防御CSRF攻击的方法,它的原理是在每个需要保护的请求中,都加上一个只有服务器和客户端知道的“暗号”。
- 同源策略是浏览器提供的一种安全机制,用于限制来自不同源的脚本对当前文档或从当前文档加载的资源进行访问。
- 最好的方法是将Token和同源策略结合起来,打造一个更坚固的防线。
实战建议:
- 在所有需要保护的请求中,都加上Token。
- 设置Cookie的
HttpOnly
、Secure
和SameSite
属性。 - 验证Referer Header(可选)。
- 定期审查你的代码,确保没有安全漏洞。
- 使用HTTPS,防止数据在传输过程中被窃取。
- 教育你的用户,提高他们的安全意识。
最后的提醒:
安全是一个持续的过程,需要我们不断学习和实践。 希望今天的讲座能帮助你更好地理解CSRF攻击,并有效地保护你的网站。 记住,安全无小事,防患于未然!
好了,今天的分享就到这里,谢谢大家! 祝大家编程愉快,安全第一!