Поводом к написанию этой статьи послужило нахождение мною уязвимости в одном довольно известном модуле. Так как по правилам обнаружения уязвимостей, я пока не вправе распространяться о деталях, то расскажу об уязвимости в общих чертах, а также о методах борьбы с ней. http://drupal.org/node/413938
Итак, подделка межсайтовых запросов (анг. Сross Site Request Forgery, или, сокращенно, CSRF): что это такое и с чем его едят.
CSRF — это вид атак на посетителей веб-сайтов, использующий недостатки протокола HTTP. Если жертва заходит на сайт, созданный злоумышленником, от её лица тайно отправляется запрос на другой сервер (например, на сервер платёжной системы), осуществляющий некую вредоносную операцию (например, перевод денег на счёт злоумышленника). Для осуществления данной атаки, жертва должна быть авторизована на том сервере, на который отправляется запрос, и этот запрос не должен требовать какого-либо подтверждения со стороны пользователя.
Данный тип атак, вопреки распространённому заблуждению, появился достаточно давно: первые теоретические рассуждения появились в 1988 году, а первые уязвимости были обнаружены в 2000 году.
Одно из применений СSRF — эксплуатация пассивных XSS, обнаруженных на другом сервере. Так же возможны отправка спама от лица жертвы и изменение каких-либо настроек учётных записей на других сайтах(например, секретного вопроса для восстановления пароля).
Живой пример
Например, нам нужно сделать небольшой модуль, который должен аяксом удалять ноды. Это можно реализовать служебной ссылкой ноды, при нажатии которой, отправляется аякс запрос на друпаловский путь. К этому пути прицеплен обработчик, который и удалет ноду. Вот примерно таким модулем все и делается:
node_destroy.module
/**
* Реализация hook_menu(). Регистрирует наш коллбек в системе меню.
*/
function node_destroy_menu() {
$menu['node/%node/destroy'] = array(
'page_callback' => 'node_destroy',
'page_arguments' => array(1),
'access_arguments' => array('administer nodes'),
'type' => MENU_CALLBACK,
);
}
/**
* Реализация коллбека.
*/
function node_destroy($node) {
if ($node->nid) {
node_delete($node->nid);
print('SUCCESS');
}
// в коллбеках для аякса почти всегда надо принудительно завершать скрипт,
// чтобы не выводить оформление сайта вместе с вашими данными
exit();
}
/**
* Реализация hook_link(). Добавляем свою ссылку в служебные ссылки ноды.
*/
function node_destroy_link($type, $node = NULL, $teaser = FALSE) {
switch ($type) {
case 'node':
// если эта функция вызывается, значит мы выводим ссылки ноды,
// а это значит, что нам и скрипты нужны
$path = drupal_get_path('module', 'node_destroy');
drupal_add_js($path .'/node_destroy.js');
// собственно, добавление ссылки
$links['node_destroy'] = array(
'title' => t('Destroy node'),
'href' => "node/$node->nid/destroy",
'attributes' => array('class' => 'node_destroy_link'),
);
break;
}
return $links;
}?>
node_destroy.js
// вместо обычного $(document).ready(function() { ... })
Drupal.behaviors.node_destroy = function(context) {
// Мы перебираем все наши ссылочки и навешиваем на них аякс запросы.
// Заметьте необычный селектор. Он предотвратит двойное навешивание обработчиков.
$('.node_destroy_link:not(.processed)', context).addClass('processed').click(function(){
href = $(this).attr('href');
$.ajax({
type: "GET",
url: href,
success: function(result){
// SUCCESS нам возвращает наш коллбек меню, если все замечательно
if (result != 'SUCCESS') {
alert('Error');
}
}
});
});
}
И все бы хорошо, но в один солнечный день, на сайт приходит злой тролль... Или более жизненная ситуация — озлобленный бывший сотрудник приходит на сайт и пытается его поломать. Помня старый опыт, он пробует зайти по адресу http://site.ru/node/123/destroy
, но получает от ворот поворот, так как уже не имеет прав на удаление материалов.
И тут, в порыве деструктивного креатива, он создает ноду с таким контетом:
Что происходит в этот момент? Никакая картинка, естественно, не подгрузится, но браузер тролля выполнит запрос на этот путь с прежним результатом.
Смирившись с неудачей, тролль уходит с сайта. Через день, администратор сайта замечает эту мусорную ноду, заходит в нее и удаляет. А вернувшись в список материалов, не находит в нем ноды с айдишником 123. Атака удалась. Занавес.
Для тех, кто не понял, когда администратор зашел в ноду, его браузер тоже ломанулся по ссылке картинки. Но здесь уже прав доступа хватило, и нода была успешно удалена, а админ даже ничего не заметил.
Как избежать CSRF уязвимостей?
Ответ — использовать уникальные ссылки для действий по изменению данных. Как это возможно? В друпале используется метод токенизации ссылок. Это означает, что к ссылке активного действия, прибавляется уникальный параметр, который проверяется при осуществлении самого действия. В друпале сгенерировать такой параметр можно функцией drupal_get_token(). Проверить —drupal_valid_token(). Токен генерируется на основе подаваемого значения, сессии пользователя, а также приватного ключа сайта, что практически сводит на ноль вероятность генерации вредителем правильного токена.
Внесем изменения в наш модуль. Начнем с выставления правильной ссылки:
function node_destroy_link($type, $node = NULL, $teaser = FALSE) {
switch ($type) {
case 'node':
$path = drupal_get_path('module', 'node_destroy');
drupal_add_js($path .'/node_destroy.js');
$links['node_destroy'] = array(
'title' => t('Destroy node'),
'href' => "node/$node->nid/destroy",
'attributes' => array('class' => 'node_destroy_link'),
// query — это все GET параметры, т.е. все что в ссылке находится после знака вопроса
// мы добавляем параметр token
'query' => 'token='. drupal_get_token('node_destroy_'. $node->nid)
);
break;
}
return $links;
}
?>
Как вы помните, мы шлем аякс запрос по адресу, который зашит в ссылке, поэтому в коллбеке нам остается только проверить $_GET
стандартным способом.
function node_destroy($node) {
if ($node->nid && isset($_GET['token']) && drupal_valid_token($_GET['token'], 'node_destroy_'. $node->nid)) {
node_delete($node->nid);
print('SUCCESS');
}
exit();
}?>
via DrupalDance
Комментарии
Этот дырявый модуль в ядре или сторонний?
Сторонний.
Поддержал на Хабре. Спасибо за разбор полётов, вовремя. Я недавно с друпалом и такие вещи в изучении незаменимая вещь. Она помогает избежать дальнейших ошибок. Еще раз спасибо, neochief!
зачет!
Спасибо, что делишься своим добытым опытом.
Еще раз спасибо!
Спасибо, принял к сведению.
Основательная работа. Спасибо. Подшил в раздел "Безопасность".
PS. Вот не знаю - чем больше разбираюсь с друпал, тем больше мне нравится как все организовано. Я даже не знал, что такой вид атак существует, а уже есть хорошее решение - токены.
Автору большое спасибо та тему, заметка очень актуальна.
старые грабли на новый лад.
раньше этим на drupal.ru и на хабре поднимали пузомерки на темы и пользователей, теперь нужно придумывать специальные модули для показа этих проблем?
за вариант обхода данной уязвимости - спасибо, но как заметили эта ошибка у стороннего модуля(пускай возможно и популярного), а на сторонних модулях найти можно как SQL-инъекции и не менее популярные XSS
они были и к сожалению будут появляться новые...
Спасибо, Александр, будем внимательнее.
спасибо, пример очень понравился
поддержал на хабре
а что если, вставить ссылку вида node/номер/delete
сработает?
конечно. Только нода не удалится. Потому как по этому пути всегда (если руками не патчить) возвращается форма - да нет
Главное, чтобы оставались люди, готовые улучшать положение вещей. Причитания точно не помогут, а накрутку можно было пофиксить еще 2 года назад. Чего ж никто не пофиксил?
А не проще сделать обработку тега IMG.
Если адрес картинки не оканчивается разрешенным расширением, то запретить запись ноды.
Хороший пример!
Думаю, выход - передавать в запросе зашифрованное число-сессию вида f(секретный код, IP пользователя, время). Заодно по истечении времени - уничтожать.
А вообще-то достаточно даже просто хэш секретного кода передавать. Главное при инсталляции (друпала или модуля) этот код первый раз генерировать уникальный.
Почитайте статью, и не выдумывайте велосипед.
Спасибо за информацию!
Не получится, там нужно подтверждение через нажатие кнопки.
Еще вопрос. А время жизни токена какое?
Кстати, по моему Вконтакте так спам рассылают, или нет?
Время жизни токена = время жизни сессии.
хорошо если внесут в ядро друпала защиту от этого, а то ведь
работает
Нда, весело админу будет. Причем удалить ноду можно будет только из admin/content/node
Вариант
- включить тригерры и проверять наличие таких подлостей.
Что там у нас еще без подтверждения работает?:)
img == XSS
Поэтому и рулят image imce и imagefield
Картинки это зло
Даешь цифро-буквенные картинки !
Спасибо! Очень интересный материал.
хм.
Если следовать неписанному стандарту, то гет запросом можно ТОЛЬКО ПОЛУЧАТЬ информацию, но не модифицировать.
Использую ПОСТ запрос, можно было бы точно так же избежать этой проблемы даже не задумываясь о ней.
jquery и post делать умеет.
вы это к чему?
я пытался сказать, что есть как минимум еще один эффективный способ борьбы с тем же самым. Причем он проистекает из "неписанных" стандартов оперирования гет и пост запросами.
нет большой разницы между POST и GET. Post запрос не на много сложнее подделать.
Другое дело, что написанное здесь можно и нужно применять и для обработки POST-форм.
Давайте говорить предметно.
Не на много сложнее говорите?
Живой пример в студию.
Я вам описываю механизм, а вы мне привеодите пример где я бы мог допустить ошибку чтобы вы смогли использовать данный вид атаки.
И так, по клику на кнопке я посылаю ПОСТ запрос который обрабатывает моя функция и удаляет ноду. Я посылаю параметров просто nid ноды. Никаких токенов не использую.
Теперь прошу, опишите мне ситуацию при которой вы бы могли сделать так, чтобы я невольно эту ноду удалил посетив чужую страницу.
Варианты с случаями где атакующий может вставить живой javascript не принимаются, потому как в таком случае ему нет смысла проводить такую атаку, а проще сразу увести мою сессию.
javaScript можно лишить возможности воровать куки, используя "httponly"
http://ru2.php.net/setcookie
http://www.php.net/manual/ru/session.configuration.php
Поэкспериментируйте:
(при изменении переменных, не забываем стирать куки и закрывать/открывать окно браузера)
<?php // Сессию PHP передавать только в куках
ini_set('session.use_only_cookies',1); // Закрыть для javaScript доступ к кукам
// (работает начиная с PHP 5.2.0)
ini_set("session.cookie_httponly", 1); // Старт сессии PHP
session_start(); // Кодировка страницы (чтоб русские буквы были видны)
header('Content-type: text/html; charset=windows-1251'); // Закрыть для javaScript доступ к кукам
// (для старых версий PHP)
// header("Set-Cookie: hidden=value; HttpOnly");
// Создаем куку (последний параметр - открывает/закрывает доступ javascript к этой куке)
setcookie('nameCookie','valueCookie',0,'','',false,true);// (работает начиная с PHP 5.2.0)
echo
'<b>Реально в куках есть:</b><br>nameCookie='.@$_COOKIE['nameCookie'].'; PHPSESSID='.@$_COOKIE['PHPSESSID'];
echo
'<script>alert("javaScript прочитал куки:\n"+document.cookie);
</script>'; ?>
Поэтому, будем считать, что javaScript не может прочитать сессию админа (если админ использует современный браузер).
Тем не менее, через метод POST, без токенов, взломать сайт можно:
Допустим в админке сайта есть визуальный редактор (в нем можно вставлять формы и скрипты) и им пользуются супер админ и помочник у которого права только на правку новостей. Так вот этот помочник, создаст в одной из новостей необходимую форму + javaScript и попросит админа взглянуть на новость, на сайте. В момент просмотра новости, незаметно, будут выполнены любые команды от имени админа. В логах, так же будет написано, например: "админ X удалил ноду Y". Если помочник уберет из новости свой код, то гореадмину будет сложно понять, что происходит.
Имхо, "токены" нужны. Особо нужны в программах PHP, которые: создают аккаунты, изменяют права, менеджеры баз данных, файлов и т.п. Ибо помочник может быть добрым, но безобразно относиться к своему паролю. А если стырят его пароль, то без особого труда испаганят не только доверенные ему новости, но и весь сайт, нахрен.
Да ну что вы прицепились к человеку со своим постом. Вам описали решение проблемы в случаях, когда POST использовать не представляется возможным. Джейквери, джейквери.. а если джаваскрипт вырублен и нужно сделать действие БЕЗ формы? Что тогда делать? Везде пихать кнопки и подтверждения? Каким тогда будет интерфейс?
И почему, раз пост такой непробиваемый по-вашему, токены автоматом вставляеются во все друпаловские формы? А я вам отвечу — потому, что это надежнее простого поста.
Вы простите ушли от темы разговора.
Безопасный код: Подделка межсайтовых запросов
тут пост практически не пробиваем. Будете спорить? Примеры в студию.
Покажите мне реальную ситуацию когда пост использовать нельзя это раз
Расскажите мне почему может быть невозможно использовать форму - это два
А то Ваши рассуждения мне очень напоминают анекдот, про студента который учил все про блох а его спросили про рыб, и он начал свой ответ что если бы у рыб была бы шерсть то в нем были бы блохи. А блохи это...
Не уходите от темы. Мы обсуждаем именно подделку межсайтовых запросов, а не пост или гет в целом.
Зачем городить огород с токенами, если все это решается банальным постом?
http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
9.1.1 Safe Methods
Implementors should be aware that the software represents the user in their interactions over the Internet, and should be careful to allow the user to be aware of any actions they might take which may have an unexpected significance to themselves or others.
In particular, the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval. These methods ought to be considered "safe". This allows user agents to represent other methods, such as POST, PUT and DELETE, in a special way, so that the user is made aware of the fact that a possibly unsafe action is being requested.
Naturally, it is not possible to ensure that the server does not generate side-effects as a result of performing a GET request; in fact, some dynamic resources consider that a feature. The important distinction here is that the user did not request the side-effects, so therefore cannot be held accountable for them.
Я не могу представить вам кода, которым можно взломать грамотно написанную POST форму. Вы это хотели услышать?
В то же время, я могу представить несколько вариантов, где форму использовать неудобно. Главный — если вы хотите упростить некие действия, не добавляя лишних подтверждений. Да, и спасибо вам за ссылку на w3, я уже там был. Это те чуваки, кторых я очень уважаю, но которые же ввели неудобную бокс-модель в xhtml.
Demimurych, отличная идея.
Если взять, к примеру, действие logout, то достаточно ссылку заменить на кнопку формы, отсылающую post-запрос с параметром(input type=hidden).
А кнопку можно стилизовать как угодно, пользователь и не догадается, что это кнопка.
И никаких подтверждений!
Нет
Код мне не обязателен.
Достаточно описания не надуманного (реального) примера, где использование форм имеет ряд существенных недостатков из которых следует что требуется использовать гет запрос.
На всякий случай еще раз повторю задачу.
Вводные, сайт работает не используя javascript ов (потому как если использует, то выбор в сторону пост очевиден не так ли?)
Необходимо выполнить по команде пользователя(клик) функцию, при этом обрисовать условия при которых организация вызова этой функции посредством пост запроса имеет ряд существенных недостатков достаточных для того чтобы принять решение в сторону гет запроса.
Идея не моя.
Это стандарт де факто для организации подобного рода запросов. Которым пренебрегают разработчики и выдумывают велосипеды в виде использования токенов.
Возможно, начитавшись этого топика, некто lut4rp накатил патч, который прислал Alexandr Shvets. Так они залатали знаменитую дыру в Vote Up/Down.
Обратите внимание на то, кто запостил репорт:
http://drupal.org/node/413938
Он же и патчи сделал, а lut4rp их всего лишь закомитил
Сам себя не похвалишь:)......
исправил.
Где Ваши примеры того почему следует использовать ГЕТ? Вместо ПОСТА?
Хочу узнать, как текст цитаты связан с выбором post/get
BTW вот вам и пример. Представьте себе галвную страницу с 10+ формами и у каждой 2 сабмита.
Текст цитаты связан с человеком, который сказал что покажет и не показал.
Да и что?
Попробуйте, вы увидите что страница от этого не страдает.
Или, если я вас не правильно понял и вас беспокоят именно серые кнопки?
Таки беспокоят. А прямоугольные цветные бесят.
А про 300% прирост html кода на from destination=... я вообще молчу
Для меня тема GET vs POST закрыта.
Demimurych, вам что-то доказывать бесполезно.
Ага, кто то любит кулаками махать, а кто то просто делает полезное дело
Дружище.
Вы тут доказать ничего не можете.
Я прошу вас примеры вы молчите.
Я дал Вам ссылки на авторитетные источники.
Я понимаю что кому то очень трудно признать что его работа это велосипед придуманный, для тех кто привык работать неправильно.
Имейте смелость хотя бы написать в своей теме, что есть как минимум еще один способ решить туже саму проблему.
Ну что решили хуями меряться?
Посмотрели бы секьюрити рассылки внимательнее что ли. Ну или бы ник поугуглили.
Какое это имеет отношение к делу?
Кто то делает дело и молчит.
А кто то размножает НЕПРАВИЛЬНЫЕ способы работы с подобного рода задачами.
1. Вы что еще не используете gzip компрессию? Сравните сжатый код страницы в 100 килобайт и страницы в 50 килобайт.
2. Кнопки не обязательно должны выглядеть так как они выглядят по умолчанию. Они могут выглядеть так, что вы никогда не помете что это кнопка пока не посмотрите в код. htmlbook.ru в руки
"типичный пользователь" может и не отличит. Он и jpg может за кнопку принять. Я - отличу. А htmlbook читать второй раз не собираюсь.
Засим прощайте.
to Demimurych: Слушайте, ни кто не собирается с Вами ни чем мериться.
Речь идет о том, что вот Вы не решили эту задачу, а орете и хамите больше всех.
И абсолютно не важно "хорошее" или "плохое" это было решение.
Это АБСОЛЮТНО не важно.
Понимаете, лучше пусть будет плохое решение, чем не будет хорошего решения.
Так что Вы сначала сделайте "хорошо", а уж потом мы это и обсудим.