Формулировка проблемы
Думаю никому из веб-разработчиков не нужно объяснять что такое 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 содержит неточности, он предназначен исключительно для демонстрации концепции, это не тот код, который можно применять в продакшене. Я также еще не проектировал систему в целом, но очевидно что она будет сохранять результат фильтрации, таким образом преобразование будет выполняться один раз — при добавлении контента. Выводится на страницу будет уже безопасная версия.
Спасибо что дочитали до конца. Надеюсь сообщество найдет эту статью полезной.
ссылка на оригинал статьи http://habrahabr.ru/post/171557/
Добавить комментарий