CC 咖啡猫的工作空间 Coding Space

前端安全

XSS(Cross-Site Scripting)

类型对比

类型 触发方式 持久性 示例场景
存储型 XSS 恶意代码经服务端存储在数据库中,其他用户访问时渲染执行 持久 评论区、用户资料、富文本编辑器、留言板
反射型 XSS 恶意代码在请求 URL 参数中,服务端不经处理直接反射到页面 非持久(需诱导用户点击链接) 搜索页显示搜索词、错误页面回显参数
DOM 型 XSS 客户端 JavaScript 在处理过程中将不可信数据插入 DOM 非持久 innerHTMLdocument.writeevallocation.hash
// DOM 型 XSS 示例 - 危险代码
const name = new URLSearchParams(location.search).get('name');
document.getElementById('welcome').innerHTML = `欢迎,${name}!`;
// 攻击: ?name=<img src=x onerror=alert('XSS')>

// ✅ 安全的写法: 使用 textContent 而非 innerHTML
document.getElementById('welcome').textContent = `欢迎,${name}!`;

XSS 防御体系

防御层 手段 说明
输出转义 HTML 实体化 <&lt;"&quot;
模板安全 使用自动转义的模板引擎 React JSX 默认转义、Vue 模板默认转义、{{{ }}} 手动开启
CSP Content-Security-Policy 头 限制可加载和执行的内容源
Cookie 安全 HttpOnly 标志 禁止 JavaScript 读取 Cookie(防御 XSS 窃取会话)
输入校验 白名单机制 只允许预期格式的输入(而非黑名单)
HTML 净化 DOMPurify 库 安全地在 DOM 中插入用户生成的 HTML

CSP(Content Security Policy)

# 严格 CSP 配置(推荐)
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';
CSP 指令 控制内容 关键值
default-src 所有资源的默认源 'self'
script-src 可执行的脚本来源 'self' 'nonce-{random}' 'strict-dynamic'(推荐严格 CSP)
style-src 样式表来源 'self' 'unsafe-inline'
img-src 图片来源 'self' https: data:
connect-src XMLHttpRequest/Fetch/WebSocket 目标 'self'
frame-ancestors 允许嵌入本页的父页面(防点击劫持) 'none'
report-uri / report-to 违规上报地址 /csp-report

DOMPurify 使用

import DOMPurify from 'dompurify';

const dirty = '<img src=x onerror=alert("xss")><p>hello</p>';
const clean = DOMPurify.sanitize(dirty);
// 结果: <p>hello</p> (移除恶意标签和事件处理)

CSRF(Cross-Site Request Forgery)

原理

用户已登录 bank.com(Cookie 已设置)
                  │
                  ▼
用户访问 evil.com ──► 自动向 bank.com/transfer?to=attacker&amount=10000 发请求
                         │
                         ▼
                      Cookie 自动携带 ──► 服务端收到合法请求(以为是用户本人)
  • 跨域请求自动携带 Cookie(包括 session cookie)
  • 攻击者无法读取响应,但能执行写操作(转账、改密)

防御方案对比

防御手段 机制 安全等级 说明
SameSite Cookie 浏览器控制 Cookie 是否在跨站请求中发送 强(Lax/Strict) 最推荐,作为第一道防线
CSRF Token 服务端生成随机 token,每次请求验证 需前后端配合,额外逻辑
Referer/Origin 校验 检查请求头的 Referer/Origin 是否在白名单 Referer 可能被屏蔽或篡改
二次确认 敏感操作要求输入密码/验证码 最强 用户体验差
模式 跨站链接点击 跨站表单提交(POST) 跨站请求(GET)
Strict 不发送 不发送 不发送
Lax(默认) 发送 不发送 发送(顶级导航)
None 发送 发送 发送(需配合 Secure)
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
Set-Cookie: CSRF-Token=xyz789; HttpOnly; Secure; SameSite=Strict

Chrome 80+ 默认 SameSite=Lax,必须手动设置 SameSite=None; Secure 才能跨站发送。

CSRF Token 流程

// 服务端生成 token 嵌入页面
<meta name="csrf-token" content="random-token-123">

// 前端每次请求携带
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
  },
  body: ...
});

点击劫持(Clickjacking)

原理

  • 攻击者使用透明 iframe 覆盖在诱饵页面(按钮)上
  • 用户实际点击的是透明 iframe 中的目标页面(如"点赞"、"转账")

防御

# 方法 1:X-Frame-Options(旧,支持度广)
X-Frame-Options: DENY           # 禁止任何 iframe 加载
X-Frame-Options: SAMEORIGIN     # 仅允许同域名 iframe 加载

# 方法 2:CSP frame-ancestors(更灵活,推荐现代浏览器)
Content-Security-Policy: frame-ancestors 'none';                    # 禁止任何
Content-Security-Policy: frame-ancestors 'self' https://trusted.com; # 白名单
手段 支持度 粒度 备注
X-Frame-Options: DENY 所有浏览器 粗(全部禁止) 旧标准,适用于简单场景
CSP frame-ancestors 现代浏览器 细(可指定多个域名) 更灵活,支持多域名白名单
JS 防框架(if(top!=self) 所有 客户端 可被绕过(noopener 等),不推荐单独使用

CORS 配置安全

配置 安全风险 推荐做法
Access-Control-Allow-Origin: * 任意域可读响应数据(如 API 返回敏感信息) 校验收到的 Origin 头并回写具体域名
Access-Control-Allow-Credentials: true + * 浏览器拒绝这种组合(规范规定) 指定具体 Origin + Credentials
未校验 Origin 任意站点可跨域请求 白名单匹配 Origin
# ❌ 不安全
Access-Control-Allow-Origin: *

# ✅ 安全
Access-Control-Allow-Origin: https://your-app.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Vary: Origin

使用 Vary: Origin 让 CDN 根据 Origin 缓存不同的响应。


HTTPS 与传输安全

HSTS(HTTP Strict Transport Security)

# 严格配置
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
参数 含义
max-age=63072000 缓存 2 年(单位秒)
includeSubDomains 所有子域名也强制 HTTPS
preload 提交到浏览器预加载列表(硬编码到浏览器,即使首次访问也是 HTTPS)
  • HSTS 生效后浏览器自动将 HTTP 转为 HTTPS
  • 首次访问风险:浏览器尚未收到 HSTS 头 → 中间人可以拦截 → 解决:HSTS Preload

证书透明(Certificate Transparency)

  • 所有公开 CA 签发的证书必须记录到公共 CT 日志
  • 浏览器验证证书是否存在于 CT 日志中
  • 防止 CA 被攻破后签发伪造证书而不被发现

依赖安全

npm audit

# 扫描已知漏洞
npm audit

# 修复(自动升级可修复的依赖)
npm audit fix

# 查看详情
npm audit --json

lockfile 固定版本

# package-lock.json 或 yarn.lock 必须提交到仓库
# 确保 CI/CD 和生产环境安装的依赖版本一致

供应链攻击类型

攻击类型 描述 案例
依赖混淆 攻击者在公共仓库发布同名包,当内部包名被拼写错误时自动安装恶意公共包 event-stream(2018)
恶意包劫持 维护者账号被黑或钓鱼,在合法包中植入恶意代码 ua-parser-js / coa / rc(2021)
原型污染 在第三方库中写入 __proto__ 属性 lodash(CVE-2019-10744)

防御措施:

  • npm audit / npm outdated 定期检查
  • 使用 socket.devsnyk 进行依赖安全扫描
  • 设置 npm config set registry 指定私有 registry 优先
  • 使用 --ignore-scripts 安装 + 手动检查 postinstall 脚本

SRI(Subresource Integrity)

<!-- CDN 引入时校验文件哈希 -->
<script
  src="https://cdn.example.com/react@18/react.production.min.js"
  integrity="sha384-..."
  crossorigin="anonymous"
></script>
  • integrity 属性指定 base64 编码的哈希值(sha384/sha512)
  • 浏览器下载后计算哈希对比,不匹配则拒绝执行
  • 必须配合 crossorigin 属性

认证与授权

JWT(JSON Web Token)

结构: header.payload.signature

// header: 算法类型
{ "alg": "HS256", "typ": "JWT" }
// payload: 声明
{ "sub": "user123", "iat": 1516239022, "exp": 1516242622 }
// signature: 对 header + payload 的签名
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

Token 存储位置对比

存储位置 XSS 风险 CSRF 风险 可用性
httpOnly Cookie 低(JS 无法读取) 需 CSRF 防御(SameSite + Token) 自动发送
localStorage 高(XSS 可窃取) 低(不会自动发送) 需 JS 手动添加 Authorization 头
内存变量 刷新页面丢失

推荐: accessToken 短时效(15min)存内存 + refreshToken 长时效(7天)存 httpOnly Cookie

双 Token 刷新策略

// 请求拦截器:自动检查 token 过期
let isRefreshing = false;
let refreshSubscribers = [];

axios.interceptors.response.use(
  response => response,
  async error => {
    if (error.response?.status === 401 && !error.config._retry) {
      if (isRefreshing) {
        // 队列等待刷新完成
        return new Promise(resolve => {
          refreshSubscribers.push(token => {
            error.config.headers.Authorization = `Bearer ${token}`;
            resolve(axios(error.config));
          });
        });
      }
      error.config._retry = true;
      isRefreshing = true;
      const { token } = await axios.post('/refresh');
      isRefreshing = false;
      error.config.headers.Authorization = `Bearer ${token}`;
      refreshSubscribers.forEach(cb => cb(token));
      refreshSubscribers = [];
      return axios(error.config);
    }
    return Promise.reject(error);
  }
);

Token 刷新策略对比

策略 描述 优点 缺点
双 Token accessToken(15min)+ refreshToken(7天) 短 token 被盗损失小 需额外刷新逻辑
滑动窗口 每次请求时将 token 过期时间后延 用户体验好 刷新频繁,服务器压力大
单 Token 短时效 token 15 分钟过期 简单 用户频繁重新登录

OAuth 2.0 流程

模式 适用场景 安全注意
授权码模式(Authorization Code) 服务端应用 最安全,code 只能使用一次
授权码 + PKCE 单页应用 / 移动端 无需 client_secret,用 code_challenge 替换
Implicit 已被 OAuth 2.1 废弃 token 在 URL 中暴露,不安全
Client Credentials 服务端到服务端 无用户参与

PKCE(Proof Key for Code Exchange): 客户端生成 code_verifier + code_challenge(SHA256),授权码换 token 时校验,即使授权码被拦截也无法换 token。

Session 管理

方案 存储位置 扩展性 缺点
服务端 sessionId + Cookie 服务端内存/Redis 需集中存储 水平扩展需共享 session 存储
JWT 客户端 好(无状态) 无法主动注销

其他安全风险

原型污染(Prototype Pollution)

// 攻击示例
const obj = {};
obj.__proto__.isAdmin = true;
// 或通过 lodash merge 等深拷贝方法
_.merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));

// 防御
// 1. 使用无原型对象
const safe = Object.create(null);
// 2. 冻结原型
Object.freeze(Object.prototype);
// 3. 校验 JSON key
function sanitize(obj) {
  delete obj.__proto__;
  delete obj.constructor;
  return obj;
}

SSRF(Server Side Request Forgery)

  • 攻击者控制服务端发起的内网请求,访问内部资源
  • 防御: 白名单 URL、禁止对内网 IP 发起请求、限制协议

ReDoS(正则拒绝服务)

// 危险正则 - 灾难性回溯
const evil = /^(a+)+b$/;
evil.test('aaaaaaaaaaaaaaaaaaaaaac'); // 指数级耗时

// 防御
// 1. 避免嵌套量词(`(a+)+` / `(a|aa)+` / `(.*)*`)
// 2. 使用 `re2` 库(拒绝回溯机制的正则引擎)
// 3. 设置正则匹配超时