前端安全体系
What — 什么是前端安全体系
前端安全体系是一套从架构层面保障 Web 应用安全的系统方案,涵盖传输安全、输入输出安全、认证授权、内容安全策略、隐私保护、依赖安全等多个维度,而非单一的漏洞防护。
安全威胁全景
| 维度 | 威胁 | 影响 |
|---|---|---|
| 传输层 | 中间人攻击、数据窃听 | 数据泄露、会话劫持 |
| 注入类 | XSS、HTML 注入、模板注入 | 脚本执行、Cookie 窃取 |
| 请求伪造 | CSRF、点击劫持 | 冒充用户操作 |
| 认证会话 | 会话固定、Token 泄露 | 身份冒充 |
| 内容安全 | 恶意脚本加载、CDN 劫持 | 代码执行、数据窃取 |
| 隐私合规 | 未授权数据收集、追踪 | 法律风险、用户流失 |
| 供应链 | 恶意依赖、包劫持 | 后门植入 |
| 客户端存储 | localStorage 篡改、存储型攻击 | 数据篡改、逻辑绕过 |
安全防御层次
┌─────────────────────────────────────┐
│ 用户浏览器 │
│ ┌───────────────────────────────┐ │
│ │ CSP / HTTPS / SRI │ │ ← 传输与内容安全
│ ├───────────────────────────────┤ │
│ │ 输入过滤 / 输出编码 / 沙箱 │ │ ← 注入防护
│ ├───────────────────────────────┤ │
│ │ CSRF Token / SameSite / CORS │ │ ← 请求安全
│ ├───────────────────────────────┤ │
│ │ Cookie Secure / Token 安全 │ │ ← 认证安全
│ ├───────────────────────────────┤ │
│ │ 存储加密 / 隐私合规 │ │ ← 数据安全
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Why — 为什么需要体系化安全
- 单点防护不够:修了 XSS 但没有 CSP,一个遗漏就全线崩溃
- 合规要求:GDPR、等保 2.0、《个人信息保护法》要求系统性保障
- 供应链风险:前端依赖数量庞大(平均 1000+),每个都是攻击面
- 攻击自动化:攻击者使用自动化工具批量扫描,体系化防御提高门槛
- 用户信任:安全事故导致用户流失,修复成本远高于预防成本
How — 怎么建设
1. 传输安全 — HTTPS
// 强制 HTTPS(HSTS)
// 服务端响应头
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
// 前端检测
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
location.replace(`https:${location.href.substring(location.protocol.length)}`)
}
# Nginx 配置 HSTS
server {
listen 443 ssl;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
混合内容:HTTPS 页面加载 HTTP 资源会被浏览器拦截(被动内容降级,主动内容阻止)。解决方案:所有资源走 HTTPS,使用 upgrade-insecure-requests:
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
2. XSS 防护
2.1 输出编码
// HTML 编码
function encodeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
// JS 编码(防止 JS 上下文注入)
function encodeJS(str) {
return str.replace(/[\\"']/g, '\\$&')
.replace(/�/g, '\\0')
.replace(/
/g, '\\u2028')
.replace(/
/g, '\\u2029')
}
// URL 编码
function encodeURL(str) {
return encodeURIComponent(str)
}
2.2 React/Vue 自动转义
// React 默认转义,安全
<div>{userInput}</div>
// dangerouslySetInnerHTML 必须消毒
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
<!-- Vue 默认转义,安全 -->
<div>{{ userInput }}</div>
<!-- v-html 必须消毒 -->
<div v-html="sanitize(userInput)"></div>
2.3 DOMPurify 消毒
import DOMPurify from 'dompurify'
// 基础用法
const clean = DOMPurify.sanitize(dirtyHTML)
// 自定义允许标签
const clean = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title'],
FORBID_TAGS: ['style', 'script'],
FORBID_ATTR: ['onerror', 'onload']
})
// 允许 data-* 属性
DOMPurify.sanitize(dirtyHTML, { ADD_ATTR: ['data-id'] })
3. CSRF 防护
// 1. SameSite Cookie(最简方案)
// 服务端设置
Set-Cookie: sessionId=xxx; SameSite=Strict; Secure; HttpOnly
// SameSite 三个值:
// Strict — 跨站请求完全不带 Cookie(最安全但影响体验)
// Lax — 导航到目标网站的 GET 请求带 Cookie(默认值,平衡安全与体验)
// None — 跨站请求也带 Cookie(必须配合 Secure)
// 2. CSRF Token
// 服务端生成 Token,前端提交时携带
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(data)
})
// 3. Double Submit Cookie
// 请求同时通过 Cookie 和 Header 传递 Token,服务端比对一致
const token = getCookie('csrf_token')
fetch('/api/data', {
method: 'POST',
headers: { 'X-CSRF-Token': token },
credentials: 'same-origin'
})
// 4. 检查 Origin / Referer
// 服务端验证请求头
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer
if (origin && !origin.startsWith('https://myapp.com')) {
return res.status(403).json({ error: 'Forbidden origin' })
}
next()
})
4. CSP(内容安全策略)
<!-- 基础 CSP -->
<meta http-equiv="Content-Security-Policy"
content="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;
font-src 'self' https://fonts.gstatic.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self'">
// 报告模式(不拦截,只报告)——先观察再启用
// Content-Security-Policy-Report-Only: ...
// 收集违规报告
// Report-To: { "url": "/api/csp-report", "max_age": 10886400 }
// 动态添加 nonce(服务端渲染)
// 每次请求生成随机 nonce
app.get('*', (req, res) => {
const nonce = crypto.randomBytes(16).toString('base64')
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'`
)
res.render('index', { nonce })
})
// HTML 中使用 nonce
// <script nonce="{{nonce}}">
// console.log('trusted code')
// </script>
CSP 关键指令:
| 指令 | 作用 | 示例 |
|---|---|---|
default-src | 所有资源默认策略 | 'self' |
script-src | JS 来源限制 | 'self' 'nonce-xxx' |
style-src | CSS 来源限制 | 'self' 'unsafe-inline' |
img-src | 图片来源限制 | 'self' data: https: |
connect-src | XHR/Fetch/WebSocket 限制 | 'self' https://api.com |
frame-ancestors | 嵌入限制(防点击劫持) | 'none' |
report-uri | 违规报告地址 | /csp-report |
5. 点击劫持防护
<!-- 方式1:X-Frame-Options(旧方案) -->
<!-- X-Frame-Options: DENY | SAMEORIGIN | ALLOW-FROM origin -->
<!-- 方式2:CSP frame-ancestors(推荐) -->
<!-- Content-Security-Policy: frame-ancestors 'none' -->
<!-- Content-Security-Policy: frame-ancestors 'self' https://trusted.com -->
// 方式3:JS 检测(兜底方案)
if (window.top !== window.self) {
window.top.location = window.self.location
}
6. 认证与会话安全
// Cookie 安全配置
// Set-Cookie: token=xxx;
// HttpOnly — JS 无法读取,防 XSS 窃取
// Secure — 仅 HTTPS 传输
// SameSite=Lax — 防 CSRF
// Path=/api — 限制作用路径
// Domain=.app.com — 限制作用域名
// JWT 安全实践
// ❌ 不安全:存 localStorage
localStorage.setItem('token', jwt)
// ✅ 安全:存 HttpOnly Cookie
// 后端 Set-Cookie 设置 HttpOnly + Secure + SameSite
// Token 刷新策略
let refreshPromise = null
async function request(url, options = {}) {
const token = getToken()
const headers = { Authorization: `Bearer ${token}`, ...options.headers }
let res = await fetch(url, { ...options, headers })
// Token 过期,自动刷新
if (res.status === 401 && !options._retry) {
if (!refreshPromise) {
refreshPromise = refreshToken().finally(() => { refreshPromise = null })
}
const newToken = await refreshPromise
headers.Authorization = `Bearer ${newToken}`
res = await fetch(url, { ...options, headers, _retry: true })
}
return res
}
7. SRI(子资源完整性)
<!-- 防止 CDN 被劫持后篡改 JS 文件 -->
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<link
rel="stylesheet"
href="https://cdn.example.com/style.css"
integrity="sha384-abc123..."
crossorigin="anonymous"
/>
# 生成 SRI 哈希
curl https://cdn.example.com/lib.js | openssl dgst -sha384 -binary | openssl base64 -A
8. 存储安全
// 敏感数据不要存 localStorage/sessionStorage
// ❌
localStorage.setItem('password', password)
localStorage.setItem('token', token)
// ✅ 敏感数据存 HttpOnly Cookie
// ✅ 非敏感数据可以存 localStorage,但要加密
import CryptoJS from 'crypto-js'
const SECRET_KEY = 'your-secret-key' // 从环境变量获取
function encrypt(data) {
return CryptoJS.AES.encrypt(JSON.stringify(data), SECRET_KEY).toString()
}
function decrypt(cipherText) {
const bytes = CryptoJS.AES.decrypt(cipherText, SECRET_KEY)
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
}
// 存储加密数据
localStorage.setItem('preferences', encrypt(userPrefs))
// 读取解密
const prefs = decrypt(localStorage.getItem('preferences'))
// IndexedDB 安全注意事项
// 1. 不存储敏感数据
// 2. 数据验证:读取后校验数据格式
// 3. 加密存储
// 4. 设置过期时间
class SecureStorage {
constructor(dbName, storeName) {
this.dbName = dbName
this.storeName = storeName
}
async set(key, value, ttl = 3600000) {
const record = {
value,
expiry: Date.now() + ttl
}
// IndexedDB 存储逻辑...
}
async get(key) {
const record = await this._getRecord(key)
if (!record) return null
if (Date.now() > record.expiry) {
await this.delete(key)
return null
}
return record.value
}
}
9. CORS 安全配置
// 服务端 CORS 配置(Koa 示例)
app.use(async (ctx, next) => {
// ❌ 不安全:允许所有来源
// ctx.set('Access-Control-Allow-Origin', '*')
// ✅ 安全:白名单校验
const allowedOrigins = [
'https://www.myapp.com',
'https://admin.myapp.com'
]
const origin = ctx.headers.origin
if (allowedOrigins.includes(origin)) {
ctx.set('Access-Control-Allow-Origin', origin)
ctx.set('Vary', 'Origin') // 防止缓存错乱
}
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
ctx.set('Access-Control-Allow-Credentials', 'true')
ctx.set('Access-Control-Max-Age', '86400') // 预检缓存 24h
if (ctx.method === 'OPTIONS') {
ctx.status = 204
return
}
await next()
})
10. 隐私合规
// Cookie 同意管理
class CookieConsent {
constructor() {
this.categories = {
necessary: true, // 必要 Cookie,默认开启
analytics: false, // 分析 Cookie,需用户同意
marketing: false // 营销 Cookie,需用户同意
}
}
init() {
const saved = localStorage.getItem('cookie_consent')
if (saved) {
this.categories = JSON.parse(saved)
this.applyConsent()
} else {
this.showBanner()
}
}
acceptAll() {
this.categories.analytics = true
this.categories.marketing = true
this.save()
}
acceptNecessary() {
this.categories.analytics = false
this.categories.marketing = false
this.save()
}
save() {
localStorage.setItem('cookie_consent', JSON.stringify({
categories: this.categories,
timestamp: Date.now()
}))
this.applyConsent()
}
applyConsent() {
if (this.categories.analytics) {
this.loadAnalytics()
}
if (this.categories.marketing) {
this.loadMarketing()
}
}
loadAnalytics() {
// 加载 Google Analytics 等
}
loadMarketing() {
// 加载广告追踪等
}
}
11. 安全响应头清单
# Nginx 安全头配置
add_header X-Content-Type-Options "nosniff" always; # 禁止 MIME 嗅探
add_header X-Frame-Options "DENY" always; # 禁止嵌入 iframe
add_header X-XSS-Protection "0" always; # 关闭旧 XSS 过滤器(CSP 替代)
add_header Referrer-Policy "strict-origin-when-cross-origin"; # 控制 Referer 泄露
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)"; # 限制浏览器 API
add_header Content-Security-Policy "default-src 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
| 响应头 | 作用 | 推荐值 |
|---|---|---|
Strict-Transport-Security | 强制 HTTPS | max-age=31536000; includeSubDomains |
X-Content-Type-Options | 禁止 MIME 嗅探 | nosniff |
X-Frame-Options | 防点击劫持 | DENY |
Content-Security-Policy | 内容安全策略 | 按需配置 |
Referrer-Policy | 控制 Referer | strict-origin-when-cross-origin |
Permissions-Policy | 限制浏览器 API | 按需禁用 |
Cross-Origin-Opener-Policy | 隔离跨域窗口 | same-origin |
Cross-Origin-Resource-Policy | 防跨域资源泄露 | same-origin |
12. 安全审计与自动化
# GitHub Actions 安全扫描
name: Security Scan
on: [push, pull_request, schedule]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# npm 依赖审计
- run: npm audit --audit-level=high
# Snyk 安全扫描
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# ESLint 安全插件
- run: npx eslint --plugin security src/
# 检查密钥泄露
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
// eslint-plugin-security 配置
// .eslintrc.js
module.exports = {
plugins: ['security'],
extends: ['plugin:security/recommended'],
rules: {
'security/detect-object-injection': 'warn',
'security/detect-non-literal-regexp': 'warn',
'security/detect-unsafe-regex': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-non-literal-fs-filename': 'warn'
}
}
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| CSP 阻止合法脚本 | 策略过严或未覆盖内联脚本 | 使用 nonce 或 Report-Only 模式调试 |
| SRI 更新麻烦 | 每次更新 CDN 文件需重新算哈希 | CI 自动生成 SRI 哈希 |
| CORS 预检频繁 | 每次跨域请求先发 OPTIONS | 设置 Access-Control-Max-Age 缓存预检 |
| SameSite 影响第三方登录 | Strict 模式下 OAuth 回调不带 Cookie | 登录流程用 Lax,其他用 Strict |
| localStorage 被篡改 | 前端存储无保护 | 关键数据加密 + 签名校验 |
| HTTPS 混合内容 | 部分资源仍是 HTTP | 全面升级 HTTPS + upgrade-insecure-requests |
安全检查清单
- 全站 HTTPS + HSTS
- Cookie 设置 HttpOnly + Secure + SameSite
- CSP 策略配置并启用
- XSS:所有用户输入输出编码,使用 DOMPurify
- CSRF:SameSite Cookie + Token 双重防护
- SRI:外部 CDN 资源加完整性校验
- 响应头:X-Content-Type-Options / X-Frame-Options / Referrer-Policy
- 认证:JWT 存 HttpOnly Cookie,实现自动刷新
- 存储:敏感数据不入 localStorage,必要数据加密
- 依赖:npm audit + Dependabot + SBOM
- 隐私:Cookie 同意管理 + 最小数据收集
- 审计:ESLint security 插件 + CI 安全扫描
面试题
1. 前端安全的全链路防护包括哪些环节?各环节如何衔接?
答:前端安全全链路包含五个环节:(1) 传输层——HTTPS + HSTS 保证数据加密传输,防止中间人攻击;(2) 入口层——CSP 限制资源加载来源,SRI 校验 CDN 资源完整性,WAF 拦截恶意请求;(3) 应用层——输入校验 + 输出编码防 XSS,CSRF Token + SameSite 防 CSRF,CORS 白名单防跨域滥用;(4) 认证层——HttpOnly Cookie 防 XSS 窃取 Token,SameSite 防 CSRF 利用 Cookie,Token 短有效期 + 刷新机制限制泄露影响;(5) 数据层——敏感数据加密存储,隐私合规(Cookie 同意、最小收集)。各环节衔接:HTTPS 是基础,没有它 Cookie 的 Secure 和 SameSite 都无效;CSP 是 XSS 的兜底防线,即使编码遗漏也能拦截脚本执行;CSRF Token 依赖 Cookie 的 SameSite 配合才完整。任何单点防护都有盲区,必须多层纵深防御。
2. CSP 的 nonce 和 hash 方式有什么区别?各自的适用场景?
答:(1) Nonce 方式——每次请求服务端生成随机字符串,CSP 头中声明 script-src 'nonce-abc123',HTML 中 <script nonce="abc123"> 标记可信脚本。只有 nonce 匹配的脚本才能执行,攻击者无法猜到 nonce 值。适合 SSR 项目,服务端每次渲染时动态生成 nonce。(2) Hash 方式——对脚本内容计算 SHA256/384/512 哈希,CSP 头中声明 script-src 'sha256-xxx'。只有内容哈希匹配的脚本才能执行。适合脚本内容固定的场景(如内联配置)。区别:nonce 每次请求不同,更灵活;hash 基于内容,脚本修改需同步更新 CSP。推荐组合:'strict-dynamic' + nonce,nonce 授权的脚本动态加载的子脚本自动信任,无需逐个声明。
3. HttpOnly Cookie 就绝对安全吗?有哪些绕过方式?
答:HttpOnly 阻止 JS 读取 Cookie,但不代表绝对安全。绕过方式:(1) CSRF——虽然读不到 Cookie 值,但浏览器仍会自动携带,攻击者可以构造请求让浏览器自动带上 Cookie;(2) 网络层嗅探——如果未配置 Secure 标志,HTTP 请求中 Cookie 明文传输,中间人可截获;(3) 子域名泄露——Cookie 的 Domain 设置为 .example.com 时,子域名 evil.example.com 的 XSS 可以跨读;(4) 服务端日志——Cookie 每次请求发送到服务端,如果服务端日志未脱敏,Cookie 值可能出现在日志中;(5) 浏览器漏洞——极少数浏览器漏洞允许绕过 HttpOnly 限制(如历史上的 UTF-7 编码绕过)。正确做法:HttpOnly + Secure + SameSite + 合理 Domain + 合理 Path,多层防护。
4. 什么是供应链攻击?前端如何防范?
答:供应链攻击是指攻击者通过篡改项目的上游依赖(npm 包)来影响所有下游用户。典型方式:(1) 恶意包——发布与知名包名近似的包(Typosquatting),如 crossenv 冒充 cross-env;(2) 包劫持——获取维护者账号控制权,在原包中注入恶意代码(event-stream 事件);(3) 依赖混淆——在公共 registry 注册与公司内部私有包同名的包;(4) Install Scripts——postinstall 钩子在安装时执行恶意命令。防范措施:(1) lockfile 冻结——CI 用 --frozen-lockfile,保证安装版本一致;(2) 精确版本——save-exact=true,避免版本范围;(3) 禁用 scripts——ignore-scripts=true;(4) npm audit——定期扫描已知漏洞;(5) 作用域 registry——私有包 @company/ 指向私有 registry,防依赖混淆;(6) SBOM——生成软件物料清单,快速定位受影响依赖;(7) Snyk/Socket——实时监控依赖安全。
5. 如何设计一个安全的前端 Token 存储和刷新方案?
答:方案设计:(1) Access Token 存 HttpOnly Cookie——JS 无法读取,防 XSS 窃取。设置 Secure(仅 HTTPS)、SameSite=Lax(防 CSRF)、Path=/api(只对 API 请求携带,减少暴露面);(2) Refresh Token 存 HttpOnly Cookie——独立于 Access Token,Path 设为 /api/auth/refresh,只有刷新接口携带;(3) 前端无感知刷新——请求 401 时自动调用刷新接口获取新 Token,对调用方透明。用 Promise 缓存防止并发刷新(多个请求同时 401 只刷新一次);(4) 刷新失败——跳转登录页;(5) CSRF 防护——虽然 Token 存 Cookie,但 SameSite=Lax + CSRF Token 双重防护。关键点:前端 JS 永远不接触 Token 明文,只通过 Cookie 自动携带。若必须前端携带(如跨域 API),则用 Authorization Header + CSRF Token,Token 存内存(不持久化),刷新时通过 iframe 或同域接口获取。
6. CSP 报告模式怎么用?如何从报告模式过渡到强制模式?
答:CSP 报告模式使用 Content-Security-Policy-Report-Only 响应头替代 Content-Security-Policy,浏览器不会拦截违规请求,只会将违规信息发送到 report-uri 指定的地址。过渡步骤:(1) 制定目标策略——梳理所有合法资源来源,编写完整 CSP 规则;(2) 启用报告模式——先设置 Report-Only,配置 report-uri 收集违规报告;(3) 分析报告——观察 1-2 周,检查是否有合法资源被误报(如第三方 SDK、统计代码的内联脚本);(4) 调整策略——根据报告逐步添加白名单,用 nonce/hash 处理必要的内联脚本;(5) 切换强制模式——确认报告为零或极少误报后,从 Report-Only 切换到 Content-Security-Policy;(6) 持续监控——保留 report-uri,实时发现新的违规。建议使用 strict-dynamic + nonce 方案,避免维护过长的白名单。
7. 前端如何检测和防御点击劫持?
答:点击劫持是攻击者将目标网站嵌入透明 iframe,覆盖在看似无害的按钮上,诱导用户点击。防御方式:(1) X-Frame-Options——HTTP 响应头 DENY(完全禁止嵌入)或 SAMEORIGIN(仅同源可嵌入)。简单有效但不够灵活;(2) CSP frame-ancestors——Content-Security-Policy: frame-ancestors 'none' 或 frame-ancestors 'self' https://trusted.com。比 X-Frame-Options 更灵活,支持白名单,是推荐方案;(3) JS 检测兜底——if (window.top !== window.self) { window.top.location = window.self.location },但攻击者可以用 sandbox 属性禁用 JS;(4) UI 层防御——关键操作(支付、删除)增加二次确认弹窗或验证码,即使被劫持点击,用户也能看到真实操作;(5) Cookie SameSite——SameSite=Strict 使得跨站 iframe 中的请求不带 Cookie,API 返回 401。
8. 如何对一个现有前端项目做安全审计?给出完整流程。
答:完整审计流程分五步:(1) 信息收集——梳理项目技术栈、第三方依赖(npm ls --all)、外部资源加载(CDN、第三方 SDK)、API 接口清单、认证方案(JWT/Session/Cookie 配置);(2) 自动化扫描——npm audit 检查依赖漏洞、snyk test 深度扫描、eslint-plugin-security 检查代码安全问题、gitleaks 检查密钥泄露、浏览器 Lighthouse 安全审计;(3) 手动代码审查——重点检查:innerHTML/v-html/dangerouslySetInnerHTML 是否消毒、eval()/new Function() 使用、URL 参数直接插入 DOM、localStorage 存储敏感数据、CORS 配置是否过宽、API 是否鉴权;(4) 渗透测试——模拟 XSS 攻击(在各输入点注入脚本)、CSRF 攻击(构造跨站请求)、点击劫持(iframe 嵌入)、Token 泄露场景;(5) 报告与修复——按风险等级(Critical/High/Medium/Low)排列发现项,制定修复优先级和计划,修复后复测。工具推荐:OWASP ZAP(自动化扫描)、Burp Suite(手动渗透)、Security Headers(检查响应头)。