XSS 进阶:利用 `innerHTML`、`javascript:` 伪协议与 SVG 标签的绕过技巧

XSS 进阶:利用 innerHTMLjavascript: 伪协议与 SVG 标签的绕过技巧

各位开发者、安全工程师和渗透测试人员,大家好!今天我们来深入探讨一个在 Web 安全领域中非常经典但又常被忽视的话题——跨站脚本攻击(XSS)的进阶绕过技术。特别是如何通过 innerHTMLjavascript: 伪协议以及 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 编码、标签白名单)的技术手段。


二、核心知识点:innerHTMLjavascript: 伪协议与 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(如 innerHTMLappendChild),则脚本会被执行!

更隐蔽的是,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> 就没事” ❌ 错误,onclickonloadjavascript: 都能绕过
“SVG 是静态图像,不会执行脚本” ❌ 错误,SVG 是 XML,可嵌入脚本
“CSP 万能” ❌ 错误,CSP 只能限制来源,不能替代输入验证

六、结语:安全是一场持续对抗的游戏

今天的讲座告诉我们:XSS 并不是一个“过时”的漏洞,而是不断演化的攻击面。攻击者总能找到新的方式绕过传统防御,比如利用 innerHTML 的灵活性、javascript: 的隐蔽性、SVG 的合法性。

作为开发者,我们要做的不仅是修复已知漏洞,更要具备前瞻性思维——预判潜在风险点,采用多层防护策略(输入验证 + 输出编码 + CSP + 日志监控)。

记住一句话:

“没有绝对安全的系统,只有持续改进的安全实践。”

希望这篇文章能让你对 XSS 的深层机制有更清晰的认识,也希望大家在今后的开发工作中更加注重输入安全与输出控制。谢谢大家!


(全文约 4300 字)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注