Использование XSLT для предотвращения XSS путем фильтрации пользовательского контента

от автора

Формулировка проблемы

Думаю никому из веб-разработчиков не нужно объяснять что такое XSS и чем он опасен. Но в то же время, многие сайты, такие как форумы, блоги, социальные сети и т.п., стремятся предоставить пользователю возможность вставлять на страницу свой контент. Для удобства неискушенных пользователей изобретаются WYSIWYG-редакторы, делающие процесс добавления красивого комментария легким и приятным. Но за всем этим фасадом скрывается угроза безопасности. Фактически любой WYSIWYG-редактор отправляет на сервер не просто текст комментария, он отправляет HTML-код. И даже если сам редактор не предусматривает использования опасных HTML-тегов (например <iframe>), то злоумышленника это не остановит — он может послать на сервер произвольный HTML-текст, который может представлять опастность для других посетителей сайта. Я думаю мало кому понравится получить в свой браузер что-то наподобие:

<script type="text/javascript">window.location="http://hardcoresex.com/";</script>

Таким образом, возникает проблема: полученный от пользователя HTML-код необходимо фильтровать. Но что значить «фильтровать»? Каким должен быть алгоритм фильтрации, чтобы не создавать необоснованных ограничений легальным пользователям, но в то же время сделать невозможной XSS-атаку со стороны злоумышленника? Увы, но HTML достаточно сложен, написать хороший парсер достаточно непросто, а любая ошибка в нем может привести к тому, что у злоумышленника появится лазейка через которую он сможет нанести удар.

Постановка задачи

Для начала я предлагаю сформулировать задачу формально. Итак, что должен сделать фильтр:

  1. Разобрать полученный HTML
  2. Применить к нему правила фильтрации, удалить или преобразовать небезопасные элементы
  3. Вернуть получившийся безопасный HTML для дальнейшей обработки

Для того чтобы разобрать HTML можно воспользоваться существующими библиотеками, например в PHP это можно сделать почти элементарно:

function htmlToDOM($html) {   $doc=new DOMDocument();   $doc->loadHTML($html);   return $doc; }

Но что делать с полученным DOM дальше? Как сформулировать какие правила нужно к нему применять? Мне хотелось получить такое решение, которое будет:

  1. Надежным. Под надежностью я понимаю прежде всего низкую вероятность ошибки в коде, которая может привести к пропуску опасных тегов, атрибутов или значений атрибутов.
  2. Универсальным. Под универсальностью я понимаю способность фильтровать HTML с произвольной степенью детальности: от «никаких тегов, только текст» до "<iframe> с атрибутом src, содержащим адрес youtube можно, остальные — нельзя" или «у тегов <p> атрибут style использовать можно, но из его значений убрать все что относится к свойствам кроме color и background-color»
  3. Легко конфигурируемым. Должна быть возможность описать эти правила понятным образом, причем простые правила должны описываться просто, без необходимости листать пять экранов галочек и выпадающих списков чтобы просто запретить все теги.

Поиск решения

Я возвращался к этой задаче время от времени, но удовлетворяющего меня решения не находил. Получалось либо очень сложно (как в настройке, так и в реализации), либо достаточно ограниченно. Решение возникло внезапно. Я обдумывал перспективы использования XSL-шаблонов для форматирования XML-контента, как меня осенило: ведь XSLT используется для трансформации документа, а значит может быть использован и для фильтрации нежелательных элементов тоже!

Решение действительно удовлетворяет сформулированным выше требованиям:

  1. Надежность. Всю работу выполняет XSLT-процессор, вероятность ошибки в котором достаточно низка, намного ниже чем в самописном решении
  2. Универсальность. С помощью XSLT можно сформулировать правила фильтрации с любой степенью детальности.
  3. Легкость конфигурации. Простое конфигурирование сводится к добавлению элементов в «белый» или «черный» список по имеющемуся шаблону. Сложные случаи, конечно, потребуют дополнительных описаний, но эта сложность возникает только если есть необходимость в тонкой настройке фильтрации. Еще одним преимуществом использования 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *