跨站脚本攻击(XSS)是一种广为人知的漏洞类型,当攻击者能将JavaScript代码注入存在漏洞的网页时便会触发。当不知情的受害者访问该页面时,注入的代码将在受害者的会话中执行。此类攻击的影响因应用程序而异,可能从无业务影响到账户接管(ATO)、数据泄露,甚至远程代码执行(RCE)。
XSS包含多种类型,如反射型、存储型和通用型。但近年来,变异型XSS因能绕过DOMPurify、Mozilla bleach、Google Caja等净化器而令人闻之色变,其影响波及众多应用程序,包括谷歌搜索。时至今日,仍有大量应用程序易受此类攻击。
那么什么是mXSS?
(我们在Insomnihack 2024演讲中也探讨过此主题:击败净化器:为何应将mXSS纳入工具箱。)
背景
作为Web开发者,您可能已集成或实现过某种净化机制来防御XSS攻击。但鲜为人知的是,构建有效的HTML净化器有多么困难。HTML 净化器的核心目标是确保用户生成内容(如文本输入或外部数据源)既不构成安全威胁,也不破坏网站或应用程序的预期功能。
实现 HTML 净化器的最大挑战在于 HTML 本身的复杂性。作为多功能标记语言,HTML 包含大量元素、属性及潜在组合,这些都可能影响网页结构与行为。在准确解析分析HTML代码的同时保留其预期功能,实属艰巨任务。
HTML
在探讨mXSS之前,让我们先了解构成网页基础的标记语言——HTML。理解HTML的结构与运作机制至关重要,因为mXSS(变异型跨站脚本攻击)正是利用了HTML的特殊机制与复杂特性。
HTML被视为一种容错性强的语言,因其在遇到错误或异常代码时具有宽容特性。不同于某些严格的编程语言,HTML即使面对不规范代码仍优先呈现内容。这种容错机制具体表现如下:
当渲染错误标记时,浏览器不会崩溃或显示错误提示,而是尽力解析并修复HTML——即便存在轻微语法错误或元素缺失。例如在浏览器中打开以下标记<p>test
即使缺少闭合标签p
仍能正常执行。查看最终页面的HTML代码可见解析器已自动修复错误标记,闭合p
元素为:<p>test</p>
。
为何需要容错性:
- 无障碍访问:网络应面向所有人开放,HTML中的小错误不应阻碍用户获取内容。容错性使更广泛的用户和开发者群体能够与网络交互。
- 灵活性:HTML使用者编程经验参差不齐。容错性允许存在某些疏漏或错误,同时确保页面功能不完全失效。
- 向后兼容性:网络持续演进,但大量现有网站仍基于旧版HTML标准构建。容错机制确保这些旧版网站即使不符合最新规范,仍能在现代浏览器中正常显示。
但我们的HTML解析器如何判断应以何种方式“修复”错误标记?<a><b>
应该变为<a></a><b></b>
还是<a><b></b></a>
?
对此已有详尽的HTML规范作为依据,但遗憾的是,规范中仍存在某些模糊点,导致当今主流浏览器之间仍存在不同的HTML解析行为。
变异
那么HTML能够容忍错误标记,这与主题有何关联?
mXSS中的M代表“变异”,HTML中的变异指因各种原因对标记所做的任何修改。
- 解析器修复错误标记(
<p>test
→<p>test</p>
)即为变异。 - 规范化属性引号(
<a alt=test>
→<a alt=”test”>
)即为变异。 - 重新排列元素(
<table><a>
→<a></a><table></table>
)即为变异 - 等等...
mXSS正是利用这种行为绕过安全过滤机制,具体示例将在技术细节部分展示。
HTML解析背景
将长达1500多页的标准
浓缩为单节内容显然不现实。但鉴于其对理解深度mXSS及有效载荷运作机制的重要性,我们仍需涵盖若干核心要点。为便于理解,我们开发了mXSS速查表(本文后文将提供),将庞杂标准浓缩为研究者和开发者更易掌握的资源。
不同内容解析类型
HTML并非万能解析环境。元素处理内容的方式各异,存在七种不同的解析模式。我们将深入解析这些模式如何影响mXSS漏洞:
- 空元素
area,
base
,br
,col
,embed
,hr
,img
,input
,link
,meta
,source
,track
,wbr
template
元素模板
- 原始文本元素
script,
style
,noscript
,xmp
,iframe
,noembed
,noframes
- 可转义原始文本元素
textarea,
title
- 外部内容元素
svg,
math
- 纯文本状态
plaintext
- 常规元素
- 所有其他允许的HTML元素均为常规元素。
通过以下示例可清晰展示解析类型的差异:
- 首个输入为
div
元素(常规元素): <div><a alt="</div><img src=x onerror=alert(1)>">
- 另一方面,第二个输入是使用
style
元素(属于“原始文本”)实现的类似标记: <style><a alt="</style><img src=x onerror=alert(1)>">
观察解析后的标记,我们能清晰看到解析差异:


div
元素的内容以HTML形式呈现,同时创建一个a
元素。看似闭合的div
标签和img
标签实为a
元素的属性值,因此作为a
元素的alt
文本呈现,而非HTML标记。在style
示例中,style
元素的内容以原始文本形式呈现,故不创建a
元素,而所谓的属性值现在属于常规HTML标记。
外部内容元素
HTML5引入了将专业内容集成到网页中的新方式。两个关键示例是<svg>
和<math>
元素。这些元素利用独立命名空间,意味着它们遵循与标准HTML不同的解析规则。理解这些不同的解析规则对于缓解与mXSS攻击相关的安全风险至关重要。
让我们以先前示例为例,但这次将其封装在svg
元素中:
<svg><style><a alt="</style><img src=x onerror=alert(1)>">

在此情况下,我们确实观察到a
元素被创建。style
元素并未遵循“原始文本”解析规则,因为它位于不同的命名空间内。当处于SVG或MathML命名空间时,解析规则会发生改变,不再遵循HTML语言规范。
攻击者可利用命名空间混淆技术(如DOMPurify 2.0.0 绕过),操纵净化器以不同于浏览器最终渲染的方式解析内容,从而规避恶意元素的检测。
从变异到漏洞
mXSS一词常被广泛用于涵盖各类净化器绕过技术。为便于理解,我们将通用术语“mXSS”细分为4个子类别
解析器差异
解析器差异虽可称为常规净化器绕过,有时也被归类为mXSS。无论何种称谓,攻击者均可利用净化器算法与渲染器(如浏览器)之间的解析差异。由于HTML解析的复杂性,解析差异并不必然意味着某一方存在错误。
以noscript元素为例,其解析规则为:“若脚本标记启用,则将标记器切换至RAWTEXT状态;否则保持数据状态。”
(链接) 这意味着
noscript
元素的正文内容会根据JavaScript启用状态呈现不同效果。在净化器阶段禁用JavaScript而渲染器阶段启用是符合逻辑的。这种行为本身并非错误,但可能导致绕过机制,例 如:
<noscript><style></noscript><img src=x onerror=”alert(1)”>
JavaScript禁用时:

JavaScript 已启用:

其他许多解析器差异也可能出现,例如不同的HTML版本、内容类型不匹配等。
解析往返现象
解析往返现象是广为人知且有文献记载的现象,其指出:“若使用HTML解析器解析该算法的输出结果,可能无法还原原始树结构。即使通过HTML解析器自身也可能生成无法通过序列化与重新解析步骤实现往返的树结构,尽管此类情况通常不符合规范。”
这意味着根据HTML标记的解析次数,生成的DOM树结构可能发生变化。
让我们参考规范中提供的官方示例:
但首先需明确:form
元素内部不能嵌套另一个form
元素:“内容模型:流式内容,但不得包含表单元素后代”(如规范所述)

但如果我们继续阅读文档,他们给出了form
元素如何通过以下标记嵌套的示例:
<form id="outer"><div></form><form id="inner"><input>
html
├── head
└── body
└── form id="outer"
└── div
└── form id="inner"
└── input
由于存在未闭合的 div
元素,</form>
将被忽略,此时 input
元素将关联到内部的 form
元素。现在,若将此树结构序列化并重新解析,<form id="inner">
起始标签将被忽略,因此 input
元素将转而关联到外部的 form
元素。
<html><head></head><body><form id="outer"><div><form id="inner"><input></form></div></form></body></html>
html
├── head
└── body
└── form id="outer"
└── div
└── input
攻击者可利用此行为在净化器与渲染器间制造命名空间混淆,从而实现如下绕过:
<form><math><mtext></form><form><mglyph><style></math><img src onerror=alert(1)>
致谢 @SecurityMB,深度解析详见 此处。
反净化
反净化是应用程序在将净化器输出发送给客户端前进行干预时犯下的关键错误,本质上是逆转了净化器的处理效果。标记的任何微小改动都可能对最终DOM树产生重大影响,从而绕过净化机制。我们曾在Insomni'Hack演讲及多篇博文中探讨过此问题,并识别出各类应用程序中的漏洞,包括:
- 脱 sanitization 陷阱:osTicket 客户数据泄露事件
- 代码漏洞致 Proton 邮件系统面临风险
- 代码缺陷引发 Tutanota 桌面端远程代码执行
- 代 码漏洞致 Skiff 邮件系统面临风险
以下是去净化示例:某应用程序将净化器输出中的svg
元素重命名为custom-svg
,此操作改变了元素的命名空间,在重新渲染时可能引发跨站脚本攻击。

上下文相关
HTML解析过程复杂,且会因上下文而异。例如,在Firefox中解析整个文档与解析片段存在差异(参见速查表中浏览器特异性部分)。开发者在处理浏览器从数据净化到渲染的转换时,可能误改数据渲染的上下文环境,导致解析差异并最终绕过净化器。由于第三方净化器无法预知结果的呈现上下文,故无法解决此问题。该问题有望通过浏览器内置净化器(净化器API项目)实现解决。
例如:应用程序对输入内容进行净化后,在将其嵌入页面时却用SVG进行封装,导致上下文变更为SVG命名空间。

mXSS案例研究
虽然我们过去曾发布过探讨mXSS漏洞的博客文章,例如《回应calc:攻破Mailspring的攻击链》,但也报告过多种 sanitizer 绕过技术,如mganss/HtmlSanitizer(CVE-2023-44390)、 Typo3(CVE-2023-38500)、OWASP/java-html-sanitizer等。
但让我们聚焦于Joplin(CVE-2023-33726)这款基于Electron的桌面笔记应用中的简单案例。由于Electron配置存在安全隐患,Joplin中的JS代码可调用Node内部功能,使攻击者能在目标机器上执行任意命令。
该漏洞源于其数据净化器的解析器——该解析器通过htmlparser2 npm包处理不可信的HTML输入。该解析器本身宣称不遵循规范且优先考虑速度而非准确性:“若需严格遵守HTML规范,请参考parse5。”我们很快发现该解析器存在多处偏离规范的行为。该包本身声明其不遵循规范且优先考虑速度而非准确性:“若需严格遵守HTML规范,请参考parse5。”
我们很快发现该解析器存在多处规范偏差。通过以下输入可观察到解析器对不同命名空间的无视:

虽然消毒器的解析器不会渲染img
元素,但渲染器会执行该操作。这体现了解析器差异性的特性 ——攻击者只需添加onerror
事件处理程序,当受害者打开恶意笔记时,该程序便会执行任意代码。

该具体发现亦由@maple3142独立验证
缓解措施
遗憾的是,目前尚无简易的统一解决方案。我们建议开发者深入理解此类漏洞,以便根据自身应用场景制定更优的缓解策略。
研究过程中,我们发现开发者采取了多种缓解方案与安全措施来应对mXSS问题(详见速查表):
客户端数据净化
- 这可能是最关键的防护准则。使用客户端运行的净化工具(如DOMPurify)可规避解析差异风险。由于解析过程的复杂性及内容需适配不同解析器(Firefox/Chrome/Safari等),当HTML在非最终渲染位置解析时,差异问题难以避免。因此服务器端净化器极易失效。
- 当在服务器端渲染(SSR)环境中使用客户端JS框架时,常会引入isomorphic-dompurify等库。这些库使DOMPurify等客户端净化器能在SSR模式下“正常工作”,但为此需引入jsdom等服务器端HTML解析器,从而引入解析器差异风险。对采用SSR的Web应用而言,最安全的方案是:对用户可控HTML禁用SSR,将内容净化与渲染完全移交至客户端处理。
避免重新解析
- 为规避“往返式mXSS”,应用可直接将净化后的DOM树插入文档,而非序列化后重新渲染内容。
- 注意:此方案仅适用于客户端实现净化器,且可能引发意外行为(例如因未适配页面上下文导致内容渲染差异)。
始终编码或删除原始内容
- 鉴于mXSS的攻击机制在于让恶意字符串在净化器中以纯文本形式呈现,却能在后续被解析为HTML,若在净化阶段禁止/编码所有原始文本,将彻底阻断其重新渲染为HTML的可能性。
- 注意:此举可能破坏某些功能(如CSS代码)。
不支持外部内容元素
- 在过滤器中不支持外部内容元素(删除svg/math元素及其内容而非重命名)能显著降低复杂度。
- 注意:此举虽不能消除mXSS风险,但可作为预防措施。
未来
如此复杂的课题没有简单解决方案,是否存在光明前景?
答案是肯定的。幸运的是,已有诸多提案和行动旨在彻底解决或至少正式应对这一漏洞类别。
当前最大问题在于:清理不可信HTML输入的责任被推给了第三方开发者——无论是应用程序开发者还是净化器开发者。由于任务复杂性,加之需适配不同渲染器解析器(用户使用不同浏览器)并持续跟进HTML规范演进,这种做法实不可行。更合理的解决方案是将确保标记中不存在恶意内容的责任归于渲染器。例如在浏览器内置净化器,就能消除迄今为止我们所见到的绝大多数(甚至全部)绕过机制。
Sanitizer API倡议正是为此而生。该项目目前由Web平台孵化器社区组(WICG)开发,旨在为开发者提供由浏览器自身编写的集成化、稳健且具备上下文感知能力的净化器(无需再处理解析器差异或重新解析)。若Sanitizer API获得更广泛的浏览器支持,开发者很可能会更频繁地使用它来实现更安全的HTML操作。
另一项应对措施是规范更新,例如Chrome现已对属性中的<
和>
字符进行编码
<svg><style><a alt="</style>">
→ <svg><style><a alt="</style>">
通过演进HTML定义的基础规范,迈向更安全的未来。
mXSS速查表 🧬🔬
我们创建了mXSS速查表,旨在为所有对mXSS领域学习、研究和创新感兴趣的人提供一站式资源。通过简化列表帮助用户快速识别意外的HTML行为,避免阅读1500多页文档。我们鼓励用户贡献力量,共同推动这项工作。
概述
mXSS(变异型跨站脚本攻击)源于HTML处理机制的漏洞。即便Web应用部署了强力过滤器防范传统XSS攻击,mXSS仍能悄然渗透。其原理在于利用HTML行为的特殊性,使恶意元素逃过过滤器的检测。
本文深入剖析mXSS机制,通过实例演示将“mXSS”这一庞大概念拆解为子模块,并详述开发者防护策略。我们期待通过知识赋能,助力开发者与研究人员未来能从容应对这一安全挑战。