Веб-разработка

индекс
236,88

XSS фильтр на основе DOMDocument

За этот пост мне не дали инвайта, поэтому после регистрации я не спешил его публиковать. Решил что тема банальна и не интересна. Но сегодня увидел топик «Не очередной XSS фильтр» и не сдержался.

Сделано на основе вот этой идеи

Когда встал вопрос об обработке постов в блоге, я первым делом попробовал использовать регулярные выражения. Пробовал я не долго, т.к. быстро понял, что в лучшем случае получится огромное нечто, напичканное сложными регэкспами. Основная проблема, кроме сложности поддержки, — контроль незакрытых тегов и неправильный их порядок.

Приведенный ниже класс (кроме того, что делает из абсолютно любой бяки валидный html код) умеет:
  1. удалять лишние теги и атрибуты
  2. проверять стили (можно ограничить список стилей и значение для каждого)
  3. удалять атрибут тега «a» со значением «javascript:“
  4. добавляет во внешние ссылки атрибут target="_blank»
  5. крайне легко расширяем
  6. все правила фильтрации основаны на списках разрешенных значений
  7. сохраняет текст, который был внутри запрещенных тегов

Первые два пункта, конечно, ключевые. Остальной функционал оставил, чтобы показать насколько просто расширять класс под свои нужды.

Преобразование к HTML-ENTITIES и обратно к UTF-8 нужно для сохранения нормального вида символов. Уже не нужно. Спасибо LastDragon!

Первый параметр — массив вида соответствий тегов и допустимых атрибутов или строка вида «cut[text], hr[class|width|size], a[href], p».

Второй параметр — соответствие «элемент» => «стили» => «допустимые значения». Допустимые значения стилей могут задаваться в виде регулярного выражения либо массива (массив оставлен, потому что in_array быстрее регулярки).

Код класса

UPD: Кстати о парсерах. Хабр-парсер наотрез отказывается воспринимать конструкцию вида <font color="#000">0</font> Если перед нулем поставить «1» — то все ок.
Попробуйте оставить ее в каментах.
+21
29 сентября 2009, 03:26
50

комментарии (50)

+1
tenshi #
> Дело в том, что метод saveHTML преобразует все символы в html сущности. Если кто знает, как избежать этого — буду рад услышать.
нужно указать кодировку. толи в конструкторе, толи метой
+1
patt #
Помоему никак, я оч долго над этим бился, прищлось писать ф-ю преобразования из html сущностей символы для latin_1. Ещё есть 'бага': класс определяет кодировку по meta и при неверном html может определить неверно и ни как по другому указать её не получается. Приходится чистить html через tidy и регекспами. «Неверный html» — то может быть html с стилями, js, тегами типа br и т.д.
+1
patt #
сори, очепятка не br, а nobr
0
cf5058 #
Посмотрите класс. Благодаря LastDragon проблема с кодировкой решена.
Список тегов и стилей можно задавать.
0
tenshi #
забавно… на локалхосте у меня всё пучком, а на хостинге такая же жопа, что и у тебя о_0"
+2
tenshi #
> удалять атрибут тега «a» со значением «javascript:“
ещё есть vbscript
кроме того, скрипты могут быть во всех ссылочных аттрибутах (тип URI в dtd). src, data и тд

+1
LastDragon #
Улучшенный вариант (не тестировал, возможны опечатки):

function process($text)
{
$text = trim(strip_tags($text, $this->valid_tags_str));
if(!$text)
return '';
$charset = $this->getTextCharset($text);
// ВАЖНО:
// 1) <meta> необходим всегда! если он пропущен кодировка будет
// определена неверно, и вместо букв получим кракозябры. Кроме того, он
// должен быть самым первым, т.к. только это гарантирует 100% правильное
// определение кодировки;
// 2) Замена "\r\n" на "\r" необходима, иначе на выходе вместо "\r" будет
// соответствующая entity (&# 13;).
$text = '<meta http-equiv="content-type" content="text/html; charset='.$charset.'" />'
$text = str_replace(array("\r\n", "\r"), "\n", $text);
// Параметры в конструкторе необязательны
$this->document = new DOMDocument('1.0', 'UTF-8');
@$this->document->loadHTML($text);
$this->cleanNode($this->document->documentElement);
$this->cleanUseXPath($this->document);

$result = $this->document->saveHTML();
$result = substr($result, stripos($result, '<body>') + 6);
$result = substr($result, 0, strripos($result, '</body>'));
return $result;
}

function getTextCharset($text)
{
// Если неизвесна, можно определить:
// 1) или из уже существующего <meta>
// 2) или из заголовков ответа
return 'UTF-8';
}

PS: огромное спасибо хабрапарсеру :(…
1) pre работает как-то странно, или я чего то не знаю?
2) &# 13; — пробел лишний (без него вставить не получилось)
+1
tenshi #
pastebin
+1
LastDragon #
Спасибо, посмотрю.
+1
LastDragon #
Зря поторопипился с комментарием :(, прошу извинить за ошибки.

Текст:

// 2) Замена "\r\n" на "\r" необходима, иначе на выходе вместо "\r" будет
// соответствующая entity (&# 13;).

следует читать:

// 2) Замена "\r\n" на "\n" необходима, иначе на выходе вместо "\r" будет
// соответствующая entity (&# 13;).

А текст:

$text = '<meta http-equiv="content-type" content="text/html; charset='.$charset.'" />'

следует читать:

$text = '<meta http-equiv="content-type" content="text/html; charset='.$charset.'" />'.$text;

Если внимательно посмотреть на код, то заметно, что вместо $this->document лучше использовать $document, т.к. единственное место в котором $this->document используется это метод cleanUseXPath(), но он туда передается в первом параметре ($document). Кроме того сейчас DOMDocument будет оставаться в памяти, до тех пор пока существует экземпляр класса StripTags.

Возможно, лучшим решением будет объявить все поля и методы (за исключением __construct() и process()) как private. В дальнейшем всегда можно открыть часть методов/полей, закрывать их потом будет намного сложнее.
0
cf5058 #
Спасибо за замечания.
Тогда наверное вот так
$text = '<head><meta http-equiv=«content-type» content=«text/html; charset='.$charset.'» /></head>'.$text;
0
cf5058 #
>> 2) Замена "\r\n" на "\r" необходима, иначе на выходе вместо "\r" будет
>> соответствующая entity (&# 13;).
Не удалось повторить. Выводится обычная \r
+1
LastDragon #
Оба замечания найдены практическим путем во время написания DOM HTML парсера.

По поводу "\r" сейчас проверил (Win XP Sp3, PHP 5.2.10) — тоже вроде правильно, но точно помню что с ним были проблемы (до этого был PHP 5.2.8 или 9, переустанавливать ради тестирования не буду). Если учесть что в приложении желательно иметь только один символ перевода строк ("\n") то это замена все равно полезна.

<meta> я всегда в начало вставляю — каких либо проблем не замечал (ну кроме дополнительной ошибки при разборе).
0
bSun #
img src="." onerror=«alert()» оно отфильтрует?
0
bSun #
Стоп, затупил, отфильтрует.
+1
underwater #
$test = new StripTags ('a[href], img[src]');
echo $test->process("123");
+1
underwater #
с первого раза не получилось.
href = data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==
0
cf5058 #
Спасибо! Я начинаю думать, что все uri атрибуты надежней пропускать через parse_url и ввести список допустимых протоколов. Есть же еще всякие skype и mailto
+1
FloppyFormator #
Мне кажется, это самое здравое решение. Ещё можно приписывать дефолтный протокол (http://) к урлам, где его не удалось определить — тогда можно даже javascript не фильтровать :)
+1
underwater #
В догонку :)


0
cf5058 #
да! это из-за магии с кодировкой ))
+1
underwater #
Именно из-за кодировки возможно как минимум три метода обойти защиту :)

if(strpos($href, 'javascript') === 0 || strpos($href, 'vbscript') === 0)

А насчет этой строчки есть сомнения. ИЕ 6 если не изменяет память! позволяет такую фишку javaTABscript. Точно не помню
+1
cf5058 #
да он (IE) издевается! А такую фишку он не позволяет: href=«http:// microsoft.com/javas_cript:alert(1)»?
+1
cf5058 #
вариант с кодировкой поправил
+1
WGH #
А разве фильтр не будет ругаться на кривой HTML?
+1
esenin #
Мне интересна производительность сего метода!
+1
regeda #
Когда я начинал писать Не очередной XSS-фильтр, идея преобразования не валидного текста к валидному XML, а потом фильтрация его с помощью XSLT/XPath отпала из-за уменьшения производительности алгоритма:
  1. скорость — теряем за счет дополнительных преобразований из HTML → XML → XSLT/XPath → HTML
  2. память — любая DOM впоследствие будет загружаться в память, которая будет расти особенно при большой вложенности тегов
0
cf5058 #
а можешь сопоставить скорость двух фильтров и выложить результаты? Подозреваю, что твой быстрей, но вот на/во сколько.
0
cf5058 #
На обработку этой страницы у меня ушло 0.2 секунды. Набор разрешенных тегов: a[href|title],script[src],h2,h1,span[class,color,style],div[class,color,style],p[class,color,style],br,font[class,color,style],table,tr,td,dd,dt,dl
Долго конечно, но обычно это не критично. Да и страницы таких размеров редко приходится обрабатывать.
0
regeda #
Если сравнивать, то сравнение должно быть в рамках одной конфигурации компьютера и операционной системы
0
regeda #
10 одновременно запущенных фильтраций заставит ждать пользователей уже 2 секудны
0
haskel #
Длинно и тяжело.
0
cf5058 #
Предложите способ проще. Я видел только на регулярных выражениях. Там еще сложнее.
+1
WGH #
Использовать XML Parser было бы лучше с точки зрения производительности, при этом код не был бы сложнее, а местами может даже и проще. Строить DOM-дерево здесь просто избыточно. Очевидный минус, что HTML должен быть валидным XML.
0
cf5058 #
Пробовал. Это не вариант. Очень много проблем возникает из-за того, что html это не xml. Да и где взять html, который будет валидным xml?
+1
WGH #
Достаточно писать XHTML, вот и всё. Отличия не такие уж существенные, заодно придется писать валидный код, что тоже плюс :)
0
cf5058 #
Может вы и правы. Просто если бы я был уверен в валидности кода, я бы лучше использовал регулярки. Там выражения достаточно простые получятся.
+1
WGH #
Однако вышеуказанный парсер предусматривает вариант и с кривым кодом тоже. В таком случае он выкидывает ошибку. В то время как при помощи регулярок проверять код на наличие XSS, основанных на вольностях браузеров, довольно проблематично.
К тому же фильтр на основе XML Parser расширить проще, чем регулярные выражения. Те же аттрибуты style иногда есть смысл обрабатывать по-умному.
+1
cf5058 #
да я не спорю. Попробуйте написать такой парсер. Может действительно все получится проще.
+1
WGH #
У меня были огрызки, вот кое-как оформил.
+1
G_Z #
проверять стили (можно ограничить список стилей и значение для каждого)
НЛО прилетело и опубликовало эту надпись здесь
0
cf5058 #
Какая разница? Почему весь тег с содержимым пропадает?
+2
FloppyFormator #
Он просто считает тег с цифрой 0 пустым тегом и вырезает его. Видимо, где-то в парсере засела конструкция вида if($tag_body).
0
artyfarty #
Кстати дико популярная ошибка, закрадывающаяся в кучу проектов. Помню в IPB чото такое находил, субмиттил баг.
0
FloppyFormator #
Вот она, нестрогая типизация :)
Не знаю, я как-то привык, хотя да, промахи были.
+1
underwater #
И снова :)



Природу этой xss я объяснить не могу, но \f почему то вырезается. Opera 9.2, IE 6.0
+1
underwater #
Ну вообще кроссбраузерно (a href java\fscript:). Видимо \f такой же символ, как и \t (не знал о таком).
+1
underwater #
src=java\01script тоже работает в ИЕ 6 и Опере. Зы — что-то я увлекся :)
+2
cf5058 #
Обновил код. Теперь если схема не в списке разрешенных — добавляем http://

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.