Nuxt4 安全防护体系
Web 安全不是可选项,而是底线。Nuxt4 作为全栈框架,攻击面比纯前端更大——既有浏览器端的 XSS、CSRF,也有服务端的注入、信息泄露。好消息是 Nuxt4 的架构天然规避了许多安全问题(Vue 的模板自动转义、Nitro 的请求隔离),再配合
nuxt-security模块和正确的编码习惯,可以构建一个坚固的安全防护体系。
1. Web 安全威胁全景
1.1 Nuxt4 应用的攻击面
| 攻击类型 | 攻击层 | 目标 | 危害等级 |
|---|---|---|---|
| XSS | 客户端 | 注入恶意脚本 | 🔴 高 |
| CSRF | 客户端→服务端 | 伪造用户请求 | 🔴 高 |
| 注入攻击 | 服务端 | SQL/NoSQL/命令注入 | 🔴 高 |
| 信息泄露 | 全栈 | 敏感数据暴露 | 🟡 中 |
| SSRF | 服务端 | 利用服务端发起内部请求 | 🟡 中 |
| 点击劫持 | 客户端 | iframe 嵌套钓鱼 | 🟡 中 |
| 中间人攻击 | 网络层 | 窃听/篡改通信 | 🔴 高 |
1.2 nuxt-security 模块
nuxt-security 是 Nuxt 生态中最全面的安全模块,一行配置开启多种防护:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-security'],
security: {
headers: {
contentSecurityPolicy: {
'script-src': ["'self'", "'unsafe-inline'"],
},
crossOriginEmbedderPolicy: 'require-corp',
xContentTypeOptions: 'nosniff',
xFrameOptions: 'DENY',
xXSSProtection: '1; mode=block',
},
requestSizeLimiter: { maxRequestSizeInBytes: 2000000 }, // 2MB
rateLimiter: { tokensPerInterval: 150, interval: 300000 },
corsHandler: { origin: ['https://example.com'] },
},
})它默认开启了安全响应头、请求限流、CORS 控制等,显著提升安全基线。
2. XSS 防御
2.1 Vue 的天然防护
Vue 的模板系统默认对所有插值表达式做 HTML 转义:
<template>
<!-- ✅ 安全:自动转义,<script> 变为 <script> -->
<p>{{ userInput }}</p>
<!-- ✅ 安全:属性绑定也会转义 -->
<div :title="userInput"></div>
</template>即使 userInput 包含 <script>alert('XSS')</script>,Vue 会将其转义为纯文本显示,不会执行。
2.2 v-html 的风险
v-html 直接插入原始 HTML,绕过了 Vue 的转义机制——这是 Nuxt 应用中 XSS 的头号风险:
<!-- ❌ 极度危险:如果 content 来自用户输入 -->
<div v-html="content"></div>使用 v-html 的安全规则:
- 永远不要直接渲染用户输入
- 如果必须渲染富文本,用 DOMPurify 过滤
// app/composables/useSanitize.ts
import DOMPurify from 'isomorphic-dompurify'
export function useSanitize() {
function sanitize(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
})
}
return { sanitize }
}<template>
<!-- ✅ 过滤后的 HTML -->
<div v-html="sanitize(article.content)"></div>
</template>
<script setup>
const { sanitize } = useSanitize()
</script>isomorphic-dompurify 同时支持 SSR 和 CSR,解决了原版 DOMPurify 依赖 window 的问题。
2.3 其他 XSS 风险点
| 风险点 | 场景 | 防护 |
|---|---|---|
| URL 注入 | <a :href="userUrl"> | 验证 URL scheme(只允许 http/https) |
| 动态组件 | <component :is="userInput"> | 白名单校验组件名 |
| 内联样式 | :style="userStyle" | 不接受用户输入的 CSS |
| eval / Function | 动态执行代码 | 永远不用 |
| SSR HTML 注入 | 服务端拼接 HTML | 使用 Vue 模板或 h() 函数 |
2.4 URL 校验
function isSafeUrl(url: string): boolean {
try {
const parsed = new URL(url)
return ['http:', 'https:'].includes(parsed.protocol)
} catch {
return false
}
}<a :href="isSafeUrl(link) ? link : '#'">{{ linkText }}</a>3. CSRF 防护
3.1 CSRF 攻击原理
跨站请求伪造(CSRF)利用浏览器自动携带 Cookie 的机制:
1. 用户登录 example.com(浏览器存储了认证 Cookie)
2. 用户访问恶意网站 evil.com
3. evil.com 的页面自动发送请求到 example.com/api/transfer
4. 浏览器自动携带 example.com 的 Cookie → 请求以用户身份执行
3.2 防护策略
策略一:SameSite Cookie
// server/api/auth/login.post.ts
setCookie(event, 'auth-token', token, {
httpOnly: true, // JS 不可读取
secure: true, // 仅 HTTPS
sameSite: 'lax', // 同站请求才携带 Cookie
maxAge: 60 * 60 * 24 * 7, // 7 天
path: '/',
})SameSite: Lax 是最佳平衡——允许导航链接携带 Cookie(用户从搜索引擎跳转过来仍保持登录),但阻止跨站 POST/PUT/DELETE 请求携带 Cookie。
策略二:CSRF Token
nuxt-security 提供了内置的 CSRF Token 机制:
// nuxt.config.ts
export default defineNuxtConfig({
security: {
csrf: {
enabled: true,
cookie: { name: '__csrf' },
},
},
})请求时自动在 Header 中携带 CSRF Token,服务端验证 Token 匹配性。
策略三:检查 Origin / Referer
// server/middleware/csrf-check.ts
export default defineEventHandler((event) => {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(event.method)) {
const origin = getRequestHeader(event, 'origin')
const allowedOrigins = ['https://example.com']
if (origin && !allowedOrigins.includes(origin)) {
throw createError({ statusCode: 403, message: 'CSRF check failed' })
}
}
})3.3 API Token 认证不受 CSRF 影响
如果你使用 Authorization: Bearer <token> 而非 Cookie 认证,CSRF 攻击天然无效——恶意网站无法获取和设置 Authorization Header。但 Bearer Token 需要存储在客户端(通常 localStorage),面临 XSS 窃取风险。
安全权衡:
| 方案 | CSRF 风险 | XSS 风险 | 推荐场景 |
|---|---|---|---|
| HttpOnly Cookie | 需要 CSRF 防护 | Token 安全 | Web 应用(推荐) |
| localStorage + Bearer | 天然免疫 | Token 可能被 XSS 窃取 | 移动端/API 客户端 |
| Cookie + Bearer 双重 | 最安全 | 复杂度高 | 高安全要求 |
4. CSP 内容安全策略
4.1 CSP 是什么
Content Security Policy(CSP)通过 HTTP 响应头告诉浏览器只允许加载指定来源的资源。即使攻击者成功注入了恶意脚本,CSP 也能阻止脚本执行。
4.2 CSP 指令详解
| 指令 | 作用 | 推荐值 |
|---|---|---|
script-src | 允许的脚本来源 | 'self' + nonce |
style-src | 允许的样式来源 | 'self' 'unsafe-inline' |
img-src | 允许的图片来源 | 'self' data: https: |
connect-src | 允许的 XHR/Fetch 目标 | 'self' https://api.example.com |
frame-ancestors | 允许嵌入的父页面 | 'none'(防止点击劫持) |
base-uri | 允许的 <base> URL | 'self' |
form-action | 允许的表单提交目标 | 'self' |
4.3 Nuxt4 CSP 配置
// nuxt.config.ts
export default defineNuxtConfig({
security: {
headers: {
contentSecurityPolicy: {
'default-src': ["'self'"],
'script-src': ["'self'", "'nonce-{{nonce}}'"], // nonce 模式
'style-src': ["'self'", "'unsafe-inline'"], // Vue SFC 需要
'img-src': ["'self'", 'data:', 'https://images.example.com'],
'connect-src': ["'self'", 'https://api.example.com'],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'frame-ancestors': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
},
},
nonce: true, // 自动为内联脚本添加 nonce
},
})4.4 CSP 与 Nuxt 的兼容挑战
| 问题 | 原因 | 解决 |
|---|---|---|
| 内联脚本被阻止 | script-src 不允许 unsafe-inline | 使用 nonce 模式(nuxt-security 自动处理) |
| 内联样式被阻止 | Vue SFC 的 <style> 会生成内联样式 | style-src 允许 unsafe-inline |
| 第三方脚本被阻止 | GA、Sentry 等外部脚本 | 添加对应域名到 script-src |
| SSR Payload 被阻止 | Nuxt 的 <script> payload | nonce 自动覆盖 |
5. HTTPS 与传输安全
5.1 强制 HTTPS
// server/middleware/https-redirect.ts
export default defineEventHandler((event) => {
if (import.meta.prod) {
const proto = getRequestHeader(event, 'x-forwarded-proto')
if (proto === 'http') {
const host = getRequestHeader(event, 'host')
return sendRedirect(event, `https://${host}${event.path}`, 301)
}
}
})5.2 HSTS 头
// nuxt.config.ts
export default defineNuxtConfig({
security: {
headers: {
strictTransportSecurity: {
maxAge: 31536000, // 1 年
includeSubdomains: true,
preload: true,
},
},
},
})HSTS 告诉浏览器在未来一段时间内只使用 HTTPS 访问该域名,即使用户输入 http://。preload 可以提交到浏览器的 HSTS 预加载列表,首次访问也强制 HTTPS。
5.3 安全响应头汇总
| 响应头 | 作用 | 推荐值 |
|---|---|---|
Strict-Transport-Security | 强制 HTTPS | max-age=31536000; includeSubDomains |
X-Content-Type-Options | 禁止 MIME 类型嗅探 | nosniff |
X-Frame-Options | 防止点击劫持 | DENY |
X-XSS-Protection | 浏览器 XSS 过滤 | 1; mode=block |
Referrer-Policy | 控制 Referer 发送 | strict-origin-when-cross-origin |
Permissions-Policy | 控制浏览器 API 权限 | 按需配置 |
6. 敏感数据处理边界
6.1 Nuxt4 的数据边界
Nuxt4 全栈架构中最关键的安全概念:server/ 目录是安全边界。
┌─────────────────────────────────┐
│ 客户端(不安全) │
│ app/, pages/, components/ │
│ ⚠️ 代码和数据对用户完全可见 │
├─────────────────────────────────┤
│ ━━━━━━ 安全边界 ━━━━━━ │
├─────────────────────────────────┤
│ 服务端(安全) │
│ server/api/, server/utils/ │
│ ✅ 代码和数据用户不可见 │
└─────────────────────────────────┘
6.2 runtimeConfig 的安全分区
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// ✅ 私有:只在 server/ 中可访问
openaiApiKey: process.env.OPENAI_API_KEY,
dbPassword: process.env.DB_PASSWORD,
jwtSecret: process.env.JWT_SECRET,
public: {
// ⚠️ 公开:会暴露到客户端 JS bundle
apiBase: process.env.API_BASE,
appName: 'AI Video Platform',
},
},
})黄金法则:API Key、数据库密码、JWT Secret 等永远放在 runtimeConfig 顶层(私有),不放在 public 中。
6.3 Payload 数据泄露
Nuxt SSR 的 Payload 会序列化到 HTML 中,客户端可以看到:
// ❌ 危险:useFetch 的数据会进入 Payload
const { data: user } = await useFetch('/api/user/me')
// user.value 包含 { id, name, email, passwordHash, internalNotes }
// passwordHash 和 internalNotes 会暴露到客户端!// ✅ 安全:API 层只返回必要字段
// server/api/user/me.get.ts
export default defineEventHandler(async (event) => {
const user = await getUserFromToken(event)
return {
id: user.id,
name: user.name,
email: user.email,
avatar: user.avatar,
// 不返回 passwordHash, internalNotes 等敏感字段
}
})6.4 环境变量安全
# .env — 不提交到 Git
OPENAI_API_KEY=sk-xxx
DB_PASSWORD=xxx
JWT_SECRET=xxx
# .env.example — 提交到 Git(不含真实值)
OPENAI_API_KEY=
DB_PASSWORD=
JWT_SECRET=确保 .env 在 .gitignore 中,生产环境通过部署平台的环境变量管理(Vercel Environment Variables、Docker secrets 等)。
7. 注入攻击防护
7.1 SQL 注入
使用 ORM(Drizzle、Prisma)而非拼接 SQL 字符串:
// ❌ SQL 注入风险
const query = `SELECT * FROM videos WHERE title LIKE '%${userInput}%'`
// ✅ 参数化查询(Drizzle)
const videos = await db.select().from(videosTable)
.where(like(videosTable.title, `%${userInput}%`))7.2 输入校验
所有用户输入都应该在服务端校验:
// server/api/videos/index.post.ts
import { z } from 'zod'
const createVideoSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(5000).optional(),
tags: z.array(z.string().max(50)).max(10),
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, createVideoSchema.parse)
// body 已经过校验和类型安全
return createVideo(body)
})readValidatedBody 是 Nitro 提供的校验辅助函数,配合 Zod 实现声明式的输入校验。
7.3 限流防护
防止暴力破解和 DDoS:
// nuxt.config.ts
export default defineNuxtConfig({
security: {
rateLimiter: {
tokensPerInterval: 100, // 每个时间窗口 100 次请求
interval: 60000, // 60 秒窗口
},
},
routeRules: {
'/api/auth/login': {
security: {
rateLimiter: {
tokensPerInterval: 5, // 登录接口更严格
interval: 300000, // 5 分钟 5 次
},
},
},
},
})8. 安全 Checklist
8.1 基础防护
- 安装并配置
nuxt-security模块 - 启用 CSP(至少
script-src: 'self'+ nonce) - 设置
X-Frame-Options: DENY - 设置
X-Content-Type-Options: nosniff - 启用 HSTS(
Strict-Transport-Security)
8.2 数据安全
- API Key/Secret 放在
runtimeConfig私有区域 -
.env文件在.gitignore中 - API 只返回必要字段,不泄露内部数据
- 所有用户输入在服务端校验(Zod)
8.3 认证安全
- Cookie 设置
HttpOnly+Secure+SameSite - 密码使用 bcrypt/argon2 哈希(不可逆)
- 登录接口限流(防暴力破解)
- JWT Secret 足够长(至少 256 位)
8.4 代码安全
- 禁止
v-html直接渲染用户输入(必须 DOMPurify) - 使用 ORM 而非拼接 SQL
- URL 绑定校验 scheme
- 不使用
eval()、new Function()
本章小结
- XSS:Vue 模板自动转义是第一道防线,
v-html必须配合 DOMPurify,URL 绑定校验 scheme - CSRF:
SameSite: LaxCookie + CSRF Token 双重防护,Bearer Token 天然免疫 - CSP:限制资源加载来源,nonce 模式兼容 Nuxt 内联脚本
- HTTPS:HSTS + 301 重定向 +
SecureCookie,全链路加密 - 数据边界:
server/是安全边界,敏感数据不进runtimeConfig.public,API 只返回必要字段 - 注入防护:ORM 参数化查询 + Zod 输入校验 + 接口限流
- nuxt-security:一个模块覆盖安全头、CSRF、限流、CORS