Формулировка проблемы
Думаю никому из веб-разработчиков не нужно объяснять что такое XSS и чем он опасен. Но в то же время, многие сайты, такие как форумы, блоги, социальные сети и т.п., стремятся предоставить пользователю возможность вставлять на страницу свой контент. Для удобства неискушенных пользователей изобретаются WYSIWYG-редакторы, делающие процесс добавления красивого комментария легким и приятным. Но за всем этим фасадом скрывается угроза безопасности. Фактически любой WYSIWYG-редактор отправляет на сервер не просто текст комментария, он отправляет HTML-код. И даже если сам редактор не предусматривает использования опасных HTML-тегов (например <iframe>), то злоумышленника это не остановит — он может послать на сервер произвольный HTML-текст, который может представлять опастность для других посетителей сайта. Я думаю мало кому понравится получить в свой браузер что-то наподобие:
<script type="text/javascript">window.location="http://hardcoresex.com/";</script>
Таким образом, возникает проблема: полученный от пользователя HTML-код необходимо фильтровать. Но что значить «фильтровать»? Каким должен быть алгоритм фильтрации, чтобы не создавать необоснованных ограничений легальным пользователям, но в то же время сделать невозможной XSS-атаку со стороны злоумышленника? Увы, но HTML достаточно сложен, написать хороший парсер достаточно непросто, а любая ошибка в нем может привести к тому, что у злоумышленника появится лазейка через которую он сможет нанести удар.
Постановка задачи
Для начала я предлагаю сформулировать задачу формально. Итак, что должен сделать фильтр:
- Разобрать полученный HTML
- Применить к нему правила фильтрации, удалить или преобразовать небезопасные элементы
- Вернуть получившийся безопасный HTML для дальнейшей обработки
Для того чтобы разобрать HTML можно воспользоваться существующими библиотеками, например в PHP это можно сделать почти элементарно:
function htmlToDOM($html) {
$doc=new DOMDocument();
$doc->loadHTML($html);
return $doc;
}
Но что делать с полученным DOM дальше? Как сформулировать какие правила нужно к нему применять? Мне хотелось получить такое решение, которое будет:
- Надежным. Под надежностью я понимаю прежде всего низкую вероятность ошибки в коде, которая может привести к пропуску опасных тегов, атрибутов или значений атрибутов.
- Универсальным. Под универсальностью я понимаю способность фильтровать HTML с произвольной степенью детальности: от «никаких тегов, только текст» до "<iframe> с атрибутом src, содержащим адрес youtube можно, остальные — нельзя" или «у тегов <p> атрибут style использовать можно, но из его значений убрать все что относится к свойствам кроме color и background-color»
- Легко конфигурируемым. Должна быть возможность описать эти правила понятным образом, причем простые правила должны описываться просто, без необходимости листать пять экранов галочек и выпадающих списков чтобы просто запретить все теги.
Поиск решения
Я возвращался к этой задаче время от времени, но удовлетворяющего меня решения не находил. Получалось либо очень сложно (как в настройке, так и в реализации), либо достаточно ограниченно. Решение возникло внезапно. Я обдумывал перспективы использования XSL-шаблонов для форматирования XML-контента, как меня осенило: ведь XSLT используется для трансформации документа, а значит может быть использован и для фильтрации нежелательных элементов тоже!
Решение действительно удовлетворяет сформулированным выше требованиям:
- Надежность. Всю работу выполняет XSLT-процессор, вероятность ошибки в котором достаточно низка, намного ниже чем в самописном решении
- Универсальность. С помощью XSLT можно сформулировать правила фильтрации с любой степенью детальности.
- Легкость конфигурации. Простое конфигурирование сводится к добавлению элементов в «белый» или «черный» список по имеющемуся шаблону. Сложные случаи, конечно, потребуют дополнительных описаний, но эта сложность возникает только если есть необходимость в тонкой настройке фильтрации. Еще одним преимуществом использования XSLT является то, что эта конфигурация может быть прочитана, понята и изменена любым разбирающимся в XSLT специалистом.
Создание фильтра с помощью XSLT
Реализация черного списка
Чтобы выяснить способна ли вообще эта идея функционировать я решил создать XSL-файл, описывающий простое копирование исходного документа в результирующий.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" encoding="utf-8"/>
<xsl:template match="@*|*">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Как можно видеть, вся суть заключается в
<xsl:template match="@*|*">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
Этот фрагмент отвечает за обработку всех элементов документа: тегов и их атрибутов. Текстовые элементы обрабатываются правилом по-умолчанию, которое просто копирует их в результирующий документ. Этим шаблоном обрабатываемый элемент также копируется в результирующий документ, а к его дочерним элементам и атрибутам рекурсивно применяются шаблоны (на самом деле все этот же универсальный шаблон). Таким образом, чтобы отфильтровать некоторые элементы нужно добавить шаблоны для них. Вот так, например, можно отфильтровать теги <script> вместе с их содержимым:
<xsl:template match="script" />
Одна строчка! Если фильтровать содержимое не нужно, то можно использовать другой вариант, например после добавления следующего фрагмента все ссылки перестанут быть таковыми:
<xsl:template match="a">
<xsl:apply-templates />
</xsl:template>
Этот фрагмент уберет теги <a>, но оставит их содержимое (которое, конечно, тоже будет повергнуто фильтрации). А вот так можно побороться с нежелательными атрибутами, например убрать у всех элементов атрибут style:
<xsl:template match="@style" />
Как видите правила просты для написания и требуют минимальных комментариев даже для незнакомого с этой системой человека. Но запихивать все что нельзя в черный список неудобно. Черный список это скорее дополнительная возможность, но ни в коем случае не защита, так как появляются новые теги, новые атрибуты и необновленные вовремя правила фильтрации могут создать угрозу сайту. Поэтому для защиты от XSS я считаю более правильным применять «белый список» (запрещено все что явно не разрешено)
Реализация белого списка
Для реализации белого списка универсальное правило нужно переписать следующим образом:
<xsl:template match="*">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="@*" />
Без дополнительных разрешающих правил оно оставит от HTML-кода только текстовые элементы, удалив все теги и их атрибуты (если не описать атрибуты отдельно — их зачения будут скопированы как текст). Чтобы разрешить, например, ссылки и картинки нужно добавить:
<xsl:template match="a|img">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
Это правило разрешит сами теги, но не их атрибуты — они будут удалены, что сделает теги бесполезными. Это легко исправить:
<xsl:template match="a/@href|img/@src">
<xsl:copy />
</xsl:template>
Это правило разрешает атрибут href у тега <a> и src у тега <img>. Поскольку у атрибутов дочерних элементов не бывает, то они просто копируются в результирующий документ. В этом правиле можно реализовать дополнительную проверку, например что ссылка ведет на объект по протоколу http:// или https:// (и таким образом избавиться от небезопасных протоколов, таких как data://):
<xsl:template match="a[@href]">
<xsl:variable name="target" select="@href" />
<xsl:choose>
<xsl:when test="starts-with($target, 'http://')">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="a/@href">
<xsl:copy/>
</xsl:template>
В этом правиле проверяется цель ссылки и в зависимости от этого принимается решение — копировать тег или нет. Теги <a> без атрибута href попадут под правило по-умолчанию и будут удалены. Аналогично можно сделать и с изображениями. Альтернативное решение — проверять значение атрибута в шаблоне атрибута, но это означает разнесение логики в два места:
<xsl:template match="a[@href]">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="a/@href">
<xsl:variable name="target" select="." />
<xsl:if test="starts-with($target, 'http://')">
<xsl:copy/>
</xsl:if>
</xsl:template>
Еще одна типичная задача — добавление ссылкам атрибута rel=«nofollow»:
<xsl:template match="a[@href]">
<xsl:copy>
<xsl:attribute name="rel">nofollow</xsl:attribute>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
Ну и наконец, самый сложный случай: манипуляция значением атрибута. Продемонстрирую решение задачи, сформулированной в требованиях — разрешить атрибут style, убрать из его значения все кроме свойств color и background-color. Сначала создадим шаблон, который анализирует значение единичного свойства и либо разрешает его использовать, либо нет:
<xsl:template name="filter-style-value">
<xsl:param name="value" />
<xsl:variable name="key" select="substring-before($value, ':')" />
<xsl:if test="($key = 'color') or ($key = 'background-color')">
<xsl:value-of select="$value" />
</xsl:if>
</xsl:template>
Теперь второй шаг: перебор всех свойств в значении и проверка каждого на допустимость:
<xsl:template name="filter-style">
<xsl:param name="value" />
<xsl:param name="filtered" select="''" />
<xsl:choose>
<!-- Проверяем содержит ли строка точку с запятой -->
<xsl:when test="contains($value, ';')">
<!-- Разбиваем на первый элемент и все остальное -->
<xsl:variable name="head" select="substring-before($value, ';')" />
<xsl:variable name="tail" select="substring-after($value, ';')" />
<!-- фильтруем первый элемент -->
<xsl:variable name="fltr">
<xsl:call-template name="filter-style-value">
<xsl:with-param name="value" select="$head" />
</xsl:call-template>
</xsl:variable>
<!-- Делаем рекурсивный вызов -->
<xsl:call-template name="filter-style">
<xsl:with-param name="value" select="$tail" />
<xsl:with-param name="filtered">
<!-- Тут приходится решить нужно ли добавлять отфильтрованный элемент (и точку с запятой или нет) -->
<xsl:choose>
<xsl:when test="string-length($fltr) > 0">
<xsl:value-of select="concat($filtered, $fltr, ';')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$filtered" />
</xsl:otherwise>
</xsl:choose>
</xsl:with-param>
</xsl:call-template>
</xsl:when>
<!-- Не содержит точку с запятой -->
<xsl:otherwise>
<!-- Фильтруем -->
<xsl:variable name="fltr">
<xsl:call-template name="filter-style-value">
<xsl:with-param name="value" select="$value" />
</xsl:call-template>
</xsl:variable>
<!-- Аналогично фрагменту выше -->
<xsl:choose>
<xsl:when test="string-length($fltr) > 0">
<xsl:value-of select="concat($filtered, $fltr, ';')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$filtered" />
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Это самый большой и сложный шаблон, но и задача нетривиальная. Его можно несколько упростить выделив повторяющийся код в еще один вспомогательный шаблон, но я не стал этого делать. Он прокомментирован, так что я думаю подробное описание его работы не требуется. Ну и последний шаблон, собственно отвечает за фильтрацию тегов:
<xsl:template match="p[@style]">
<xsl:variable name="style" select="@style" />
<xsl:copy>
<xsl:attribute name="style">
<xsl:call-template name="filter-style">
<xsl:with-param name="value" select="@style"/>
</xsl:call-template>
</xsl:attribute>
<xsl:apply-templates />
</xsl:copy>
</xsl:template>
Заключение
Таким образом, я считаю что максимально близко подошел к заявленной цели — созданию надежного и гибкого фильтра для вводимого пользователем контента. Сразу хочу оговориться — приведенный XSL содержит неточности, он предназначен исключительно для демонстрации концепции, это не тот код, который можно применять в продакшене. Я также еще не проектировал систему в целом, но очевидно что она будет сохранять результат фильтрации, таким образом преобразование будет выполняться один раз — при добавлении контента. Выводится на страницу будет уже безопасная версия.
Спасибо что дочитали до конца. Надеюсь сообщество найдет эту статью полезной.
Автор: Akela_wolf