各位听众,大家好!我是今天的主讲人。今天咱们来聊聊前端安全那些事儿,保证让大家听得懂、学得会、用得上,以后写代码也能更安心!
前端安全攻防战:CSRF、XSS、点击劫持,一个都别想跑!
前端安全,听起来高大上,实际上就是保护咱们用户的隐私和数据不被坏人偷走或者篡改。这年头,网络安全威胁可不小,咱们前端工程师得像个战士一样,守好这道防线。今天,我们就重点聊聊三个常见的攻击:CSRF、XSS 和点击劫持,以及如何在 JavaScript 代码层面进行防范。
第一战:CSRF(跨站请求伪造)——“李鬼”冒充“李逵”
啥是 CSRF?
CSRF,英文全称 Cross-Site Request Forgery,翻译过来就是“跨站请求伪造”。简单来说,就是攻击者伪装成你的用户,偷偷地向你的服务器发送请求,执行一些你用户并不想执行的操作。
想象一下:你登录了某个银行网站,正在浏览账户余额。这时,你点开了一个恶意链接,这个链接指向银行网站的转账接口,并带上了你的 Cookie 信息。你的浏览器一看,这个请求是发给银行网站的,而且带着你的 Cookie,就屁颠屁颠地发送了过去。银行服务器一看,请求来自你的浏览器,Cookie 也匹配,就以为是你本人发起的请求,于是就按照链接里的指令,把你的钱转到了攻击者的账户里。
是不是想想都后怕?
CSRF 攻击原理
CSRF 攻击之所以能成功,是因为它利用了浏览器的“同源策略”的漏洞。浏览器在发送请求时,会自动带上与目标域名相关的 Cookie 信息。攻击者只要诱骗用户访问包含恶意请求的页面,就能利用用户的 Cookie,冒充用户向服务器发送请求。
如何防御 CSRF?
防御 CSRF 的关键在于,让服务器能够区分请求是否来自真正的用户操作。以下是一些常见的防御方法:
-
使用 CSRF Token
CSRF Token 是一种常见的防御方法。它的原理是,在用户访问页面时,服务器生成一个随机的 Token,并将它放在 Session 或者 Cookie 中。同时,将这个 Token 嵌入到表单或者链接中。当用户提交表单或者点击链接时,浏览器会将 Token 一起发送给服务器。服务器验证 Token 的有效性,如果 Token 不正确,就拒绝请求。
代码示例(前端):
// 获取 CSRF Token (假设 Token 已经通过某种方式传递到前端,比如隐藏的 input 字段) const csrfToken = document.querySelector('input[name="_csrf"]').value; // 创建一个 XMLHttpRequest 对象 const xhr = new XMLHttpRequest(); // 设置请求头,将 CSRF Token 包含在请求头中 xhr.setRequestHeader('X-CSRF-Token', csrfToken); // 发送请求 xhr.open('POST', '/api/transfer', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = function() { // 处理响应 }; xhr.send(JSON.stringify({ amount: 100, toAccount: 'attacker' }));
代码示例(后端,Node.js + Express):
const express = require('express'); const csrf = require('csurf'); const cookieParser = require('cookie-parser'); const app = express(); // 使用 cookie-parser 中间件 app.use(cookieParser()); // 使用 csrf 中间件 const csrfProtection = csrf({ cookie: true }); app.use(csrfProtection); // 渲染包含 CSRF Token 的页面 app.get('/transfer', (req, res) => { res.send(` <form action="/transfer" method="POST"> <input type="hidden" name="_csrf" value="${req.csrfToken()}"> <input type="number" name="amount" placeholder="Amount"> <input type="text" name="toAccount" placeholder="To Account"> <button type="submit">Transfer</button> </form> `); }); // 处理转账请求 app.post('/transfer', (req, res) => { if (req.body._csrf !== req.csrfToken()) { return res.status(403).send('CSRF validation failed'); } // 处理转账逻辑 res.send('Transfer successful!'); }); app.listen(3000, () => { console.log('Server listening on port 3000'); });
注意事项:
- Token 的生成必须是随机的、不可预测的。
- Token 必须保存在服务器端,防止泄露。
- Token 必须在使用后失效,防止重放攻击。
- 对于 AJAX 请求,需要将 Token 放在请求头中,而不是放在请求体中,以防止被浏览器缓存。
-
验证 HTTP Referer 字段
HTTP Referer 字段记录了请求的来源地址。服务器可以验证 Referer 字段,判断请求是否来自合法的域名。如果 Referer 字段为空或者不是合法的域名,就拒绝请求。
代码示例(后端,Node.js + Express):
const express = require('express'); const app = express(); app.post('/transfer', (req, res) => { const referer = req.headers.referer; if (!referer || !referer.startsWith('http://your-domain.com')) { return res.status(403).send('Invalid Referer'); } // 处理转账逻辑 res.send('Transfer successful!'); }); app.listen(3000, () => { console.log('Server listening on port 3000'); });
注意事项:
- Referer 字段可以被伪造,因此这种方法并不是完全可靠。
- 某些浏览器可能会禁用 Referer 字段,导致验证失败。
-
使用双重 Cookie 验证
双重 Cookie 验证的原理是,在用户访问页面时,服务器生成一个随机的 Token,并将它放在 Cookie 中。同时,将这个 Token 嵌入到表单或者链接中。当用户提交表单或者点击链接时,浏览器会将 Cookie 中的 Token 和表单中的 Token 一起发送给服务器。服务器验证两个 Token 是否一致,如果一致,就认为请求是合法的。
代码示例(前端):
// 获取 Cookie 中的 CSRF Token function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); } const csrfToken = getCookie('csrfToken'); // 创建一个 XMLHttpRequest 对象 const xhr = new XMLHttpRequest(); // 设置请求头,将 CSRF Token 包含在请求头中 xhr.setRequestHeader('X-CSRF-Token', csrfToken); // 发送请求 xhr.open('POST', '/api/transfer', true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onload = function() { // 处理响应 }; xhr.send(JSON.stringify({ amount: 100, toAccount: 'attacker' }));
代码示例(后端,Node.js + Express):
const express = require('express'); const cookieParser = require('cookie-parser'); const crypto = require('crypto'); const app = express(); // 使用 cookie-parser 中间件 app.use(cookieParser()); // 设置 CSRF Token app.use((req, res, next) => { if (!req.cookies.csrfToken) { const csrfToken = crypto.randomBytes(64).toString('hex'); res.cookie('csrfToken', csrfToken, { httpOnly: true, secure: true }); } next(); }); // 处理转账请求 app.post('/transfer', (req, res) => { const csrfTokenFromCookie = req.cookies.csrfToken; const csrfTokenFromHeader = req.headers['x-csrf-token']; if (!csrfTokenFromCookie || !csrfTokenFromHeader || csrfTokenFromCookie !== csrfTokenFromHeader) { return res.status(403).send('CSRF validation failed'); } // 处理转账逻辑 res.send('Transfer successful!'); }); app.listen(3000, () => { console.log('Server listening on port 3000'); });
注意事项:
- Cookie 必须设置
httpOnly
属性,防止被 JavaScript 读取。 - Cookie 必须设置
secure
属性,只允许在 HTTPS 连接中传输。
- Cookie 必须设置
-
使用 SameSite Cookie 属性
SameSite Cookie 属性可以限制 Cookie 的作用域,防止跨站请求携带 Cookie。它可以设置为以下三个值:
Strict
: 只有在同一站点下的请求才会携带 Cookie。Lax
: 在同一站点下的请求以及部分跨站请求(例如<a>
标签发起的 GET 请求)会携带 Cookie。None
: 允许所有请求携带 Cookie。但是,如果设置为None
,必须同时设置secure
属性。
代码示例(后端,Node.js + Express):
const express = require('express'); const cookieParser = require('cookie-parser'); const app = express(); // 使用 cookie-parser 中间件 app.use(cookieParser()); // 设置 Cookie app.get('/set-cookie', (req, res) => { res.cookie('myCookie', 'myValue', { sameSite: 'Strict', secure: true }); res.send('Cookie set'); }); app.listen(3000, () => { console.log('Server listening on port 3000'); });
注意事项:
- SameSite Cookie 属性的兼容性不是很好,需要考虑浏览器的兼容性。
CSRF 防御总结
防御方法 | 优点 | 缺点 |
---|---|---|
CSRF Token | 安全性高,能够有效防止 CSRF 攻击。 | 需要在每个表单和链接中添加 Token,增加了开发成本。需要保证 Token 的安全性,防止泄露。 |
Referer 验证 | 实现简单,成本低。 | Referer 字段可以被伪造,安全性较低。某些浏览器可能会禁用 Referer 字段,导致验证失败。 |
双重 Cookie 验证 | 安全性较高,能够有效防止 CSRF 攻击。 | 需要维护两个 Cookie,增加了复杂性。Cookie 必须设置 httpOnly 属性,防止被 JavaScript 读取。Cookie 必须设置 secure 属性,只允许在 HTTPS 连接中传输。 |
SameSite Cookie 属性 | 实现简单,能够有效防止跨站请求携带 Cookie。 | 兼容性不是很好,需要考虑浏览器的兼容性。如果设置为 None ,必须同时设置 secure 属性。 |
第二战:XSS(跨站脚本攻击)——“木马”潜入“卧室”
啥是 XSS?
XSS,英文全称 Cross-Site Scripting,翻译过来就是“跨站脚本攻击”。它是一种代码注入攻击,攻击者通过在网页中注入恶意的脚本代码,当用户浏览网页时,这些脚本代码会被执行,从而窃取用户的 Cookie、session 等敏感信息,或者篡改网页内容,甚至控制用户的浏览器。
想象一下:你正在浏览一个论坛,看到一个帖子,里面包含了一段看似无害的代码。当你浏览这个帖子时,这段代码被执行,偷偷地把你的 Cookie 发送到了攻击者的服务器上。攻击者利用你的 Cookie,就可以冒充你登录论坛,发布恶意信息,甚至修改你的个人资料。
XSS 攻击原理
XSS 攻击之所以能成功,是因为网站没有对用户输入的数据进行充分的验证和过滤,导致攻击者注入的脚本代码被当做正常的 HTML 代码执行。
XSS 分为三种类型:
-
存储型 XSS(Persistent XSS)
存储型 XSS 是最危险的一种 XSS 攻击。攻击者将恶意脚本存储在服务器的数据库中,当用户访问包含恶意脚本的页面时,恶意脚本会被执行。
攻击流程:
- 攻击者在网站的留言板、评论区等地方发布包含恶意脚本的内容。
- 恶意脚本被存储在服务器的数据库中。
- 其他用户访问包含恶意脚本的页面时,恶意脚本被执行。
-
反射型 XSS(Reflected XSS)
反射型 XSS 是一种非持久性的 XSS 攻击。攻击者将恶意脚本作为请求参数发送给服务器,服务器将恶意脚本作为响应内容返回给浏览器,浏览器执行恶意脚本。
攻击流程:
- 攻击者构造包含恶意脚本的 URL。
- 攻击者诱骗用户点击该 URL。
- 浏览器向服务器发送包含恶意脚本的请求。
- 服务器将恶意脚本作为响应内容返回给浏览器。
- 浏览器执行恶意脚本。
-
DOM 型 XSS(DOM-based XSS)
DOM 型 XSS 是一种基于 DOM 的 XSS 攻击。攻击者通过修改 DOM 树,将恶意脚本注入到网页中。
攻击流程:
- 攻击者构造包含恶意脚本的 URL。
- 攻击者诱骗用户点击该 URL。
- 浏览器执行 JavaScript 代码,修改 DOM 树。
- 恶意脚本被注入到网页中。
如何防御 XSS?
防御 XSS 的关键在于,对用户输入的数据进行充分的验证和过滤,防止攻击者注入恶意脚本。以下是一些常见的防御方法:
-
输入验证
对用户输入的数据进行验证,只允许输入符合预期格式的数据。例如,如果用户输入的是邮箱地址,就验证是否符合邮箱地址的格式。如果用户输入的是数字,就验证是否为数字。
代码示例(前端):
const emailInput = document.getElementById('email'); const email = emailInput.value; // 验证邮箱地址的格式 const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/; if (!emailRegex.test(email)) { alert('Invalid email address'); return; }
代码示例(后端,Node.js + Express):
const express = require('express'); const validator = require('validator'); const app = express(); app.post('/register', (req, res) => { const email = req.body.email; // 验证邮箱地址的格式 if (!validator.isEmail(email)) { return res.status(400).send('Invalid email address'); } // 处理注册逻辑 res.send('Registration successful!'); }); app.listen(3000, () => { console.log('Server listening on port 3000'); });
-
输出编码
对用户输入的数据进行编码,将特殊字符转换为 HTML 实体。例如,将
<
转换为<
,将>
转换为>
,将"
转换为"
,将'
转换为'
,将&
转换为&
。代码示例(前端):
function escapeHtml(str) { const entityMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/' }; return String(str).replace(/[&<>"'/]/g, function (s) { return entityMap[s]; }); } const userInput = '<script>alert("XSS");</script>'; const escapedInput = escapeHtml(userInput); document.getElementById('output').innerHTML = escapedInput; // 显示 "<script>alert("XSS");</script>"
代码示例(后端,Node.js + Express):
const express = require('express'); const h