跨站脚本攻击(XSS)的变异形式:基于 Mutation XSS (mXSS) 的进阶利用详解
字数 4002 2025-12-13 09:19:13

跨站脚本攻击(XSS)的变异形式:基于 Mutation XSS (mXSS) 的进阶利用详解

描述
Mutation XSS (mXSS) 是一种特殊且高级的跨站脚本攻击。它与传统XSS的根本区别在于,恶意负载在初始的HTML解析和过滤阶段是“无害”的,但当受害者的浏览器在渲染时,其HTML解析器对DOM进行了“修正”或“规范化”操作,意外地改变了原始HTML的结构,从而将原本无害的字符串“变异”成了可执行的JavaScript代码。这种攻击尤其危险,因为它能绕过依赖于服务器端或客户端初始化净化(Sanitization)的安全控制(如白名单过滤、HTML编码)。攻击者通常利用浏览器在innerHTMLouterHTML等属性操作,或某些特定HTML解析上下文中的行为差异来触发这种变异。

循序渐进讲解

第一步:理解核心前提——浏览器HTML解析器的“修正”行为
浏览器的HTML解析器并非简单地逐字解释HTML字符串。为了保证页面结构正确,当遇到畸形、不完整或特定上下文下的HTML时,它会尝试自动修正(normalize)DOM树。这是mXSS存在的根本原因。

  • 经典示例:标签嵌套与属性上下文
    • 考虑一个简单的过滤策略:它只允许<b>标签,并移除其他所有标签。假设攻击者输入为:<b><style></b><img src=x onerror=alert(1)>
    • 服务器端过滤后,移除了<img>标签,得到:<b><style></b>
    • 从服务器返回的、看起来无害的HTML片段是:<b><style></b>。这里<style>标签被<b>标签包裹,这在HTML规范中是非法的(<style>不能是<b>的子元素),但浏览器在解析时会尝试修正它。

第二步:剖析变异过程
浏览器接收到<b><style></b>后,其解析过程如下:

  1. 初始解析:解析器看到<b>,创建一个<b>元素节点。
  2. 遇到嵌套的<style>:解析器尝试在<b>元素内部创建一个<style>元素。但HTML规范不允许某些特定标签(如<style>, <title>, <textarea>等)内部再包含其他标签。当这些标签被打开后,解析器进入一种特殊的“原始文本元素”模式,它会将其后的所有内容(包括看起来像标签的字符串)都视为纯文本,直到遇到对应的闭合标签。
  3. 修正行为触发:然而,在上面的字符串中,<style>的闭合标签</style>并不存在。我们有的是</b>。为了处理这个不匹配,浏览器的修正算法可能会采取以下步骤之一(不同浏览器或版本可能不同,这是mXSS利用的关键):
    • 一种常见的修正策略是:为了正确闭合<style>标签,解析器可能会将后面的</b>视为</style>的闭合标签,并在内存的DOM树中将其“重写”。
    • 最终在内存中形成的DOM结构可能变成:<b><style></style></b>。注意,这里的</style>是浏览器“补全”的,而原始的</b>标签被“消耗”掉了。但原始的HTML字符串本身没有改变。

第三步:从无害到有害的关键一跳——innerHTML序列化
mXSS攻击的触发点通常在于JavaScript操作DOM,特别是读取元素的innerHTML属性。当你通过element.innerHTML获取一个已经修正过的DOM节点的HTML字符串时,浏览器执行的是“序列化”操作,它将内存中的DOM树转换回HTML字符串。

  • 在上面的例子中,假设我们有一个div,其innerHTML被设置为服务器返回的<b><style></b>
  • 浏览器解析后,内存DOM被修正为<b><style></style></b>
  • 如果此时,页面上的JavaScript代码(可能是第三方库、框架或应用逻辑)执行了类似document.getElementById(‘myDiv’).innerHTML的操作,它得到的不是原始的<b><style></b>,而是浏览器序列化后的结果<b><style></style></b>

第四步:构造攻击链——利用序列化差异注入代码
攻击者需要找到一个场景,使这个新序列化的、包含了“完整”<style>标签的字符串,被重新当作HTML解析并插入到DOM的另一个位置。

  1. 构造初始Payload:攻击者不会直接输入<b><style></b>,而是会输入更精妙的构造,例如:<b><style></style><img src=x onerror=alert(1)>? 不,这样<img>会被过滤掉。更聪明的做法是:<b><style><img src="x" onerror=alert(1)></style>
  2. 绕过初始过滤:一个简单的过滤器看到<b><style>都在白名单内,可能允许整个字符串通过。返回的HTML片段是:<b><style><img src="x" onerror=alert(1)></style>
  3. 触发变异与二次解析
    • 当这段HTML首次被插入到页面时(例如通过div.innerHTML = serverResponse),浏览器解析它。在<style>标签内部,<img ...>被当作纯文本处理,onerror属性不会被执行。此时是安全的。
    • 随后,页面上某个JavaScript逻辑(例如一个富文本编辑器库、一个模板引擎、或者一个“清理后再显示”的函数)读取了这个元素的innerHTML。此时,浏览器序列化DOM,返回的字符串取决于其内部表示。在某些浏览器的实现中,为了“正确地”序列化<style>内的内容,它可能会对内容进行HTML编码或进行其他转换。但关键在于,<style>标签本身被正确处理了。
    • 如果这个序列化后的字符串,又被另一个不信任<style>标签的上下文(或者一个不同的解析路径)所处理,危险就可能发生。例如,一个后续的清理函数可能认为<style>标签是危险的并移除它,但它处理的是序列化后的字符串。假设序列化结果是<b>&lt;style&gt;&lt;img src=”x“ onerror=alert(1)&gt;&lt;/style&gt;</b>,清理函数移除<style>标签及其内容,但可能因为字符串的编码状态而操作失误。更典型的mXSS利用是,序列化后的字符串被意外地拼接并插入到一个允许<img>标签的上下文中,并且此时<style>的“保护壳”在序列化/再解析过程中被打破了,导致内部的<img>标签被当作真实的HTML标签解析,从而触发onerror中的JavaScript。

第五步:一个简化的攻击案例流程

  1. 用户输入评论:<b><style><img src="x" onerror=alert(1)></style>
  2. 服务器端白名单过滤器(允许<b>, <style>)放行。
  3. 页面用commentDiv.innerHTML = userComment显示评论。此时,<style>内的<img>是文本,不执行。
  4. 页面有一个“编辑评论”功能。点击编辑时,JavaScript通过originalContent = commentDiv.innerHTML获取当前内容以填充编辑器。
  5. originalContent现在包含了浏览器序列化后的DOM字符串。假设在某种浏览器中,这个序列化过程改变了字符串的表示。
  6. 编辑器(或一个“预览”功能)将这个字符串再次设置为某个元素的innerHTML。在这次新的解析中,由于字符串表示的细微变化,浏览器可能不再将<img>识别为<style>内的文本,而是将其作为一个独立的、有效的HTML标签进行解析。
  7. onerror事件被触发,XSS执行。

防御措施

  1. 永远不要使用innerHTML/outerHTML来处理不信任的数据:这是最根本的原则。使用安全的API,如textContent来设置文本,或使用经过严格安全审计的模板引擎/库(如React, Vue, Angular,它们默认进行编码)。
  2. 在服务器端进行净化,并在客户端保持一致:如果必须在客户端操作HTML,使用成熟的、专门设计用来防御mXSS的净化库(如DOMPurify)。这些库了解浏览器的变异行为,并采取额外步骤(例如,在内存DOM中检查,而不仅仅是字符串过滤)来确保安全。
  3. 谨慎处理序列化与反序列化:避免将已解析的DOM节点序列化(innerHTML)后,再将其结果作为HTML重新解析并插入到文档的其他部分。如果需要移动节点,使用DOM方法(如appendChild, insertBefore)直接操作节点对象。
  4. 实施严格的内容安全策略(CSP):即使发生mXSS,一个禁止内联脚本执行(‘unsafe-inline’)的CSP策略可以阻止大部分攻击payload执行。
  5. 保持浏览器更新:浏览器厂商会持续修复导致mXSS的解析器怪异行为。使用最新版本的浏览器可以减少攻击面。

总结
mXSS利用了浏览器HTML解析器的“修正”行为与innerHTML序列化行为之间的差异,将看似无害、已通过过滤的静态内容,在动态的DOM操作过程中“变异”为可执行代码。防御的核心在于深刻理解“浏览器解析HTML”与“字符串处理HTML”之间的本质区别,并采用基于DOM操作而非字符串拼接的安全开发模式。

跨站脚本攻击(XSS)的变异形式:基于 Mutation XSS (mXSS) 的进阶利用详解 描述 Mutation XSS (mXSS) 是一种特殊且高级的跨站脚本攻击。它与传统XSS的根本区别在于,恶意负载在初始的HTML解析和过滤阶段是“无害”的,但当受害者的浏览器在渲染时,其HTML解析器对DOM进行了“修正”或“规范化”操作,意外地改变了原始HTML的结构,从而将原本无害的字符串“变异”成了可执行的JavaScript代码。这种攻击尤其危险,因为它能绕过依赖于服务器端或客户端初始化净化(Sanitization)的安全控制(如白名单过滤、HTML编码)。攻击者通常利用浏览器在 innerHTML 、 outerHTML 等属性操作,或某些特定HTML解析上下文中的行为差异来触发这种变异。 循序渐进讲解 第一步:理解核心前提——浏览器HTML解析器的“修正”行为 浏览器的HTML解析器并非简单地逐字解释HTML字符串。为了保证页面结构正确,当遇到畸形、不完整或特定上下文下的HTML时,它会尝试自动修正(normalize)DOM树。这是mXSS存在的根本原因。 经典示例:标签嵌套与属性上下文 考虑一个简单的过滤策略:它只允许 <b> 标签,并移除其他所有标签。假设攻击者输入为: <b><style></b><img src=x onerror=alert(1)> 服务器端过滤后,移除了 <img> 标签,得到: <b><style></b> 从服务器返回的、看起来无害的HTML片段是: <b><style></b> 。这里 <style> 标签被 <b> 标签包裹,这在HTML规范中是非法的( <style> 不能是 <b> 的子元素),但浏览器在解析时会尝试修正它。 第二步:剖析变异过程 浏览器接收到 <b><style></b> 后,其解析过程如下: 初始解析 :解析器看到 <b> ,创建一个 <b> 元素节点。 遇到嵌套的 <style> :解析器尝试在 <b> 元素内部创建一个 <style> 元素。但HTML规范不允许某些特定标签(如 <style> , <title> , <textarea> 等)内部再包含其他标签。当这些标签被打开后,解析器进入一种特殊的“原始文本元素”模式,它会将其后的所有内容(包括看起来像标签的字符串)都视为纯文本,直到遇到对应的闭合标签。 修正行为触发 :然而,在上面的字符串中, <style> 的闭合标签 </style> 并不存在。我们有的是 </b> 。为了处理这个不匹配,浏览器的修正算法可能会采取以下步骤之一(不同浏览器或版本可能不同,这是mXSS利用的关键): 一种常见的修正策略是:为了正确闭合 <style> 标签,解析器可能会 将后面的 </b> 视为 </style> 的闭合标签 ,并在内存的DOM树中将其“重写”。 最终在内存中形成的DOM结构可能变成: <b><style></style></b> 。注意,这里的 </style> 是浏览器“补全”的,而原始的 </b> 标签被“消耗”掉了。但原始的HTML字符串本身没有改变。 第三步:从无害到有害的关键一跳—— innerHTML 序列化 mXSS攻击的触发点通常在于JavaScript操作DOM,特别是读取元素的 innerHTML 属性。当你通过 element.innerHTML 获取一个已经修正过的DOM节点的HTML字符串时,浏览器执行的是“序列化”操作,它将内存中的DOM树转换回HTML字符串。 在上面的例子中,假设我们有一个 div ,其 innerHTML 被设置为服务器返回的 <b><style></b> 。 浏览器解析后,内存DOM被修正为 <b><style></style></b> 。 如果此时,页面上的JavaScript代码(可能是第三方库、框架或应用逻辑)执行了类似 document.getElementById(‘myDiv’).innerHTML 的操作,它得到的 不是 原始的 <b><style></b> ,而是浏览器序列化后的结果 <b><style></style></b> 。 第四步:构造攻击链——利用序列化差异注入代码 攻击者需要找到一个场景,使这个新序列化的、包含了“完整” <style> 标签的字符串,被重新当作HTML解析并插入到DOM的另一个位置。 构造初始Payload :攻击者不会直接输入 <b><style></b> ,而是会输入更精妙的构造,例如: <b><style></style><img src=x onerror=alert(1)> ? 不,这样 <img> 会被过滤掉。更聪明的做法是: <b><style><img src="x" onerror=alert(1)></style> 绕过初始过滤 :一个简单的过滤器看到 <b> 和 <style> 都在白名单内,可能允许整个字符串通过。返回的HTML片段是: <b><style><img src="x" onerror=alert(1)></style> 。 触发变异与二次解析 : 当这段HTML首次被插入到页面时(例如通过 div.innerHTML = serverResponse ),浏览器解析它。在 <style> 标签内部, <img ...> 被当作纯文本处理, onerror 属性不会被执行。此时是安全的。 随后,页面上某个JavaScript逻辑(例如一个富文本编辑器库、一个模板引擎、或者一个“清理后再显示”的函数) 读取了这个元素的 innerHTML 。此时,浏览器序列化DOM,返回的字符串取决于其内部表示。在某些浏览器的实现中,为了“正确地”序列化 <style> 内的内容,它可能会对内容进行HTML编码或进行其他转换。但关键在于, <style> 标签本身被正确处理了。 如果这个 序列化后的字符串 ,又被另一个不信任 <style> 标签的上下文(或者一个不同的解析路径)所处理,危险就可能发生。例如,一个后续的清理函数可能认为 <style> 标签是危险的并移除它,但它处理的是序列化后的字符串。假设序列化结果是 <b>&lt;style&gt;&lt;img src=”x“ onerror=alert(1)&gt;&lt;/style&gt;</b> ,清理函数移除 <style> 标签及其内容,但可能因为字符串的编码状态而操作失误。更典型的mXSS利用是,序列化后的字符串被意外地拼接并插入到一个允许 <img> 标签的上下文中,并且此时 <style> 的“保护壳”在序列化/再解析过程中被打破了,导致内部的 <img> 标签被当作真实的HTML标签解析,从而触发 onerror 中的JavaScript。 第五步:一个简化的攻击案例流程 用户输入评论: <b><style><img src="x" onerror=alert(1)></style> 服务器端白名单过滤器(允许 <b> , <style> )放行。 页面用 commentDiv.innerHTML = userComment 显示评论。此时, <style> 内的 <img> 是文本,不执行。 页面有一个“编辑评论”功能。点击编辑时,JavaScript通过 originalContent = commentDiv.innerHTML 获取当前内容以填充编辑器。 originalContent 现在包含了浏览器序列化后的DOM字符串。假设在某种浏览器中,这个序列化过程改变了字符串的表示。 编辑器(或一个“预览”功能)将这个字符串 再次 设置为某个元素的 innerHTML 。在这次新的解析中,由于字符串表示的细微变化,浏览器可能不再将 <img> 识别为 <style> 内的文本,而是将其作为一个独立的、有效的HTML标签进行解析。 onerror 事件被触发,XSS执行。 防御措施 永远不要使用 innerHTML / outerHTML 来处理不信任的数据 :这是最根本的原则。使用安全的API,如 textContent 来设置文本,或使用经过严格安全审计的模板引擎/库(如React, Vue, Angular,它们默认进行编码)。 在服务器端进行净化,并在客户端保持一致 :如果必须在客户端操作HTML,使用成熟的、专门设计用来防御mXSS的净化库(如DOMPurify)。这些库了解浏览器的变异行为,并采取额外步骤(例如,在内存DOM中检查,而不仅仅是字符串过滤)来确保安全。 谨慎处理序列化与反序列化 :避免将已解析的DOM节点序列化( innerHTML )后,再将其结果作为HTML重新解析并插入到文档的其他部分。如果需要移动节点,使用DOM方法(如 appendChild , insertBefore )直接操作节点对象。 实施严格的内容安全策略(CSP) :即使发生mXSS,一个禁止内联脚本执行( ‘unsafe-inline’ )的CSP策略可以阻止大部分攻击payload执行。 保持浏览器更新 :浏览器厂商会持续修复导致mXSS的解析器怪异行为。使用最新版本的浏览器可以减少攻击面。 总结 mXSS利用了浏览器HTML解析器的“修正”行为与 innerHTML 序列化行为之间的差异,将看似无害、已通过过滤的静态内容,在动态的DOM操作过程中“变异”为可执行代码。防御的核心在于深刻理解“浏览器解析HTML”与“字符串处理HTML”之间的本质区别,并采用基于DOM操作而非字符串拼接的安全开发模式。