# Обход защитных мер - Mutation XSS

Когда мы ранее говорили о санитизации, я напомнил всем не пытаться реализовать её самостоятельно, а использовать существующие библиотеки. Это связано с тем, что при реализации фильтраций существует много подводных камней.

Но есть ли у этих библиотек проблемы? Это возможно, и на самом деле это уже происходило. Одна из распространенных атак на санитайзеры называется **мутационный XSS**, также известный как **mXSS**.

Прежде чем понять **mXSS**, давайте посмотрим, как обычно работают санитайзеры.

### **Basic Flow of Sanitizers** <a href="#id-obkhodzashitnykhmermutationxss-basicflowofsanitizers" id="id-obkhodzashitnykhmermutationxss-basicflowofsanitizers"></a>

Исходя из нашего предыдущего опыта, входными данными для санитайзера является строка, содержащая **HTML**, и выходными данными также является строка, содержащая **HTML**. Вот пример того, как это используется:

**const inputHtml = '\<h1>hello\</h1>'const safeHtml = sanitizer.sanitize(inputHtml)document.body.innerHTML = safeHtml**

Итак, как работает санитайзер внутри? На самом деле его внутренняя работа очень похожа на санитайзер, который мы реализовали с использованием **BeautifulSoup**:

**1. Преобразовать inputHtml в DOM-дерево.**\
**2. Удалить недопустимые узлы и атрибуты на основе файла конфигурации.**\
**3. Преобразовать DOM-дерево обратно в строку.**\
**4. Вернуть строку.**

Этот процесс, кажется, не вызывает проблем, но дьявол кроется в деталях. Что, если "HTML, который кажется безопасным, на самом деле таковым не является"? Подождите, разве мы уже не санитизировали его? Как он может быть небезопасен? Давайте сначала рассмотрим пример.

### **Browser's "Considerate" Feature** <a href="#id-obkhodzashitnykhmermutationxss-browsers-considerate-feature" id="id-obkhodzashitnykhmermutationxss-browsers-considerate-feature"></a>

Браузер - это внимательное программное обеспечение, которое, чтобы справиться с различными ситуациями и соответствуя спецификациям, может не отображать **HTML** именно так, как вы его видите. Например, рассмотрим следующий пример:

{% code overflow="wrap" %}

```html
<!DOCTYPE html><html><body>  <div id=content></div>  <script>    content.innerHTML = '<table><h1>hello</h1></table>'  </script></body></html>
```

{% endcode %}

Поместить **\<h1>** внутрь **\<table>** кажется нормальным, но если вы откроете эту веб-страницу, заметите:

<figure><img src="https://4101843849-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9aPqL1pJrSP93yQXPt21%2Fuploads%2FFMmJFZvkg6fnOpfb9nGy%2F%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png?alt=media&#x26;token=9d49ebb7-57c4-4812-ab42-c8ccc47ad4c2" alt=""><figcaption></figcaption></figure>

Структура HTML изменилась!

Она становится:

```html
<h1>hello</h1><table></table>
```

**\<h1>**, который должен был быть внутри **\<table>**, "выпрыгивает" из него. Это происходит, потому что браузер, исходя из спецификации **HTML**, определяет, что **\<h1>** не должен быть внутри **\<table>**, поэтому он любезно убирает его. Исходя из истории развития веба, для браузеров нормально пытаться исправить недействительный **HTML**. В конце концов, это лучше, чем выбрасывание ошибки или отображение пустой страницы.

Это поведение "HTML-строки изменяются браузером при рендеринге" **называется мутацией**. И **XSS,** достигнутый за счет использования этого поведения, естественно, называется **мутационным XSS**.

Рассмотрим еще один пример:

{% code overflow="wrap" %}

```html
<!DOCTYPE html><html><body>  <div id=content></div>  <script>    content.innerHTML = '<svg><p>hello</svg>'  </script></body></html>
```

{% endcode %}

Результат рендеринга:

<figure><img src="https://4101843849-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9aPqL1pJrSP93yQXPt21%2Fuploads%2FY11mBPP2zGLL9ARwjcYw%2F%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png?alt=media&#x26;token=bae31106-1bb4-419d-b95e-7de56e8fd6c9" alt=""><figcaption></figcaption></figure>

Браузер считает, что **\<p>** не должен быть внутри **\<svg>**, поэтому он перемещает **\<p>** из **\<svg>** и также исправляет **HTML**, добавляя **\</p>**.

А как насчет этого еще более странного примера? На этот раз, вместо **\<p>**, это **\</p>**:

{% code overflow="wrap" %}

```html
<!DOCTYPE html><html><body>  <div id=content></div>  <script>    content.innerHTML = '<svg></p>hello</svg>'  </script></body></html>
```

{% endcode %}

Результат:

```html
<svg><p></p>hello</svg>
```

Браузер автоматически исправляет **\</p>**, добавляя перед ним **\<p>**, но тег все равно остается внутри **\<svg>**.

> **(Примечание: Поведение браузера Chrome было исправлено, и теперь будет \<svg>\</svg>\<p>\</p>hello. Поэтому, в настоящее время, мы не можем воспроизвести эту ситуацию, но продолжим.)**

Теперь, происходит что-то интересное. **Если мы берем \<svg>\<p>\</p>hello\</svg>** и передаем его **innerHTML**, каков будет результат?

{% code overflow="wrap" %}

```html
<!DOCTYPE html><html><body>  <div id=content></div>  <script>    content.innerHTML = '<svg><p></p>hello</svg>'    console.log(content.innerHTML)  </script></body></html>
```

{% endcode %}

Результат:

```html
<svg></svg><p></p>hello
```

Не только **\<p>**, но даже следующий **"hello"** выпрыгивает. Все, что было первоначально внутри **\<svg>**, теперь находится вне его.

Итак, как эта серия изменений помогает нам обойти санитайзер? Для этого требуется сочетание с процессом санитайзера, упомянутым ранее.

Допустим, наш **inputHtml** выглядит так: **\<svg>\</p>hello\</svg>**. Первый шаг санитайзера - преобразовать его в DOM-дерево. Исходя из предыдущего эксперимента, это становится:

```html
<svg>  <p></p>  hello</svg>
```

Он выглядит абсолютно нормально, ничего не нужно фильтровать. Следующий шаг - преобразовать DOM-дерево обратно в строку, что дает: **\<svg>\<p>\</p>hello\</svg>**.

Далее, команда фронтенд-разработчиков получает **safeHtml** и выполняет **document.body.innerHTML = safeHtml**. Получающийся HTML выглядит следующим образом:

```html
<svg></svg><p></p>hello
```

Для санитайзера **\<p>** и **"hello"** находятся внутри SVG, но финальный результат другой. Они размещены снаружи. Таким образом, через этую мутацию мы можем заставить любой элемент выпрыгнуть из **\<svg>**.

Вы может спросить: "И что? В чем польза этого?" Вот где дело становится интересным

### **The Magic of HTML** <a href="#id-obkhodzashitnykhmermutationxss-themagicofhtml" id="id-obkhodzashitnykhmermutationxss-themagicofhtml"></a>

Тег **\<style>** - это волшебный тег, потому что все, что находится внутри этого тега, интерпретируется как текст. Например:

```html
<!DOCTYPE html><html><body>  <style>    <a id="test"></a>  </style></body></html>
```

Интерпретируется как:

<figure><img src="https://4101843849-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9aPqL1pJrSP93yQXPt21%2Fuploads%2FjNZijR6csaGdPC0cUFti%2F%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png?alt=media&#x26;token=d9eb12a9-a68a-4a33-94c2-ccafb92c09c9" alt=""><figcaption></figcaption></figure>

Черный текст соответственно представляет собой текст .

Но вот интересная часть. Если мы добавим внешний **\<svg>**, то браузер интерпретирует его по-другому, и все меняется. Текущий исходный **HTML-код**:

{% code overflow="wrap" %}

```html
<!DOCTYPE html><html><body>  <svg>    <style>      <a id="test"></a>    </style>  </svg></body></html>
```

{% endcode %}

В результате интерпретируется:

<figure><img src="https://4101843849-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9aPqL1pJrSP93yQXPt21%2Fuploads%2FprX74hmXyLxGv7SsopSr%2F%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png?alt=media&#x26;token=4abfaa60-4155-42eb-a4b9-69f7b3917639" alt=""><figcaption></figcaption></figure>

Тег **\<a>** внутри **\<style>** становится настоящим HTML-элементом, а не просто текстом.

Еще интереснее то, что вы можете построить следующий HTML:

```html
<svg>  <style>    <a id="</style><img src=x onerror=alert(1)>"></a>  </style></svg>
```

И это будет отображаться как:

<figure><img src="https://4101843849-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9aPqL1pJrSP93yQXPt21%2Fuploads%2FqSUhNw1uMzGKTpIrVIXS%2F%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png?alt=media&#x26;token=9681d98b-1f41-44aa-8805-55c31c15ad48" alt=""><figcaption></figcaption></figure>

Здесь мы просто добавили идентификатор **\<a>** со значением **\</style>\<img src=x onerror=alert(1)>**. Хотя он содержит **\</style>**, он не закрывает предыдущий **\<style>**. Вместо этого, он становится частью атрибута **id**. То же самое касается тега **\<img>**. Это не новый тег, а часть содержимого атрибута.

Однако, если мы удалим **\<svg>** и изменить на:

```html
<style>  <a id="</style><img src=x onerror=alert(1)>"></a></style>
```

Поскольку **\<a>** больше не является элементом, а просто текстом, у него нет атрибутов. Таким образом, **\</style>** здесь закроет предыдущий **\<style>**, в результате чего получится:

<figure><img src="https://4101843849-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F9aPqL1pJrSP93yQXPt21%2Fuploads%2FjCOPo6ik3GIoUiWLLnEt%2F%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png?alt=media&#x26;token=5080e3ba-dcfb-49bd-a9c3-af24d51f8fbb" alt=""><figcaption></figcaption></figure>

Тег **\<img>** внутри идентификатора **\<a>** изначально был лишь частью содержимого атрибута, но теперь, из-за предшествующего **\</style>**, он представлен как собственный HTML элемент.

Исходя из приведенных выше экспериментов, можно сделать вывод, что наличие **\<svg> в \<style>** важно, поскольку это влияет на интерпретацию браузером.

### **Объединяем все вместе** <a href="#id-obkhodzashitnykhmermutationxss-obedinyaemvsevmeste" id="id-obkhodzashitnykhmermutationxss-obedinyaemvsevmeste"></a>

Мы упомянули в начале об изменении поведения браузера, которое позволяет нам "вытащить все элементы из **\<svg>**". Мы также упоминали, что **"наличие \<svg> для \<style> важно"**. Комбинируя эти два понятия, мы можем получить **mXSS**.

19 сентября 2019 года **DOMPurify** выпустила версию 2.0.1, чтобы исправить уязвимость **mXSS**, которая обошла проверки с помощью мутаций. Проблемный в данном случае код был:

```html
<svg></p><style><a id="</style><img src=1 onerror=alert(1)>">
```

После преобразования этого в DOM-древо, структура становится:

{% code overflow="wrap" %}

```html
<svg>  <p></p>  <style>    <a id="</style><img src=1 onerror=alert(1)>"></a>  </style></svg>
```

{% endcode %}

Браузер делает здесь несколько вещей:

**1. Преобразует \</p> в \<p>\</p>**\
**2. Автоматически закрывает теги \<svg>, \<style>, и \<a>**

Затем **DOMPurify** проверяет на основе этого DOM-дерева. Поскольку **\<svg>**, **\<p>**, **\<style>**, и **\<a>** — все это разрешенные теги, и **id** является разрешенным атрибутом, все в порядке. Поэтому она возвращает сериализованный результат

{% code overflow="wrap" %}

```html
<svg>  <p></p>  <style>    <a id="</style><img src=1 onerror=alert(1)>"></a>  </style></svg>
```

{% endcode %}

Затем пользовательская программа передает эту строку в **innerHTML**, и происходят вышеупомянутые мутации. Все теги выбрасываются из **\<svg>**, в результате чего получается:

```html
<svg></svg><p></p><style><a id="</style><img src=1 onerror=alert(1)>"></a></style>
```

Поскольку **\<style>** также выбрасывается, элемент **\<a>** больше не существует и становится простым текстом. В результате, **\</style>** преждевременно закрывается, что приводит к тому, что скрытый **\<img>** становится настоящим HTML-элементом внутри содержимого атрибута. Это в конечном итоге приводит к **XSS**.

### **Решение проблемы** <a href="#id-obkhodzashitnykhmermutationxss-reshenieproblemy" id="id-obkhodzashitnykhmermutationxss-reshenieproblemy"></a>

Чтобы исправить эту проблему, **DOMPurify** добавил [проверку](https://github.com/cure53/DOMPurify/commit/ae16278018e7055c82d6a4ec87132fea3e236e30#diff-ac7cd96b8f4b994868af43ac8aff25573dd7cede1aab33fdcfd438811c7e853d) в код, чтобы предотвратить подверженность **mXSS**.

В то же время, эта проблема была также сообщена в **Chromium**, потому что она была связана с ошибкой парсера, которая вызывала эту странную мутацию:[Issue 1005713: Security: Parser bug can introduce mXSS and HTML sanitizers bypass](https://bugs.chromium.org/p/chromium/issues/detail?id=1005713#c_ts1574850321). **В результате, в ходе обсуждения разработчики обнаружили, что это поведение вполне соответствует спецификации, что означает, что это была ошибка в спецификации HTML!**

Таким образом, эта проблема стала вопросом исправления самой спецификации, и они открыли вопрос в репозитории спецификации: [Unmatched p or br inside foreign context needs a special parser rule #5113](https://github.com/whatwg/html/issues/5113)

Конечный результат заключался в том, что в спецификацию было добавлено новое правило, и **Chromium** исправил эту уязвимость на основе нового правила.

Так что потом похожие уязвимости больше не встречались, и все жили счастливо и долго… или все же нет?

Нет, позже было обнаружено, что у **DOMPurify** был более сложный метод обхода, но после его исправления он стал ещё сильнее и проблемы в основном не возникали.

Человек, который обнаружил эту проблему, был Michał Bentkowski, старший эксперт по безопасности в области разработки веб-безопасности. Он сообщал о различных больших и маленьких проблемах и имеет глубокое понимание парсинга HTML и различных механизмов. Позже мы увидим некоторые из классических уязвимостей, которые он сообщал.

Если вы хотите углубиться в эту проблему, вы можете обратиться к статьям, которые он написал ранее. Мои знания о **mXSS** происходят от него:

1\. [Write-up of DOMPurify 2.0.0 bypass using mutation XSS](https://research.securitum.com/dompurify-bypass-using-mxss/)\
2.[ Mutation XSS via namespace confusion – DOMPurify < 2.0.17 bypass](https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/)\
3\. [HTML sanitization bypass in Ruby Sanitize < 5.2.1](https://research.securitum.com/html-sanitization-bypass-in-ruby-sanitize-5-2-1/)

### **Заключение** <a href="#id-obkhodzashitnykhmermutationxss-zaklyuchenie" id="id-obkhodzashitnykhmermutationxss-zaklyuchenie"></a>

Когда я впервые столкнулся с **mXSS**, я был запутан и не полностью его понял. Для написания этой статьи я снова прошёл через контекст и попробовал сам, и тогда мне показалось, что я понял, что происходит. Понимание его концепции не сложно, но вникнуть во все детали потребует немного больше времени. Более того, обнаруженные уязвимости уже были исправлены, поэтому их невозможно воспроизвести в текущих браузерах, что немного проблематично.

Но в целом, я думаю, что **mXSS** - это более продвинутая тема внутри XSS. Она включает в себя спецификацию HTML, парсинг браузера и работу санитайзеров. Это нормально, что на её понимание требуется немного больше времени.
