Безопасный код в Друпале: Подделка межсайтовых запросов



    (ч2. Работа с базой данных; ч3. Работа с пользовательским вводом)

    Поводом к написанию этой статьи послужило нахождение мною уязвимости в одном довольно известном модуле. Так как по правилам обнаружения уязвимостей, я пока не вправе распространяться о деталях, то расскажу об уязвимости в общих чертах, а также о методах борьбы с ней.

    Итак, подделка межсайтовых запросов (анг. Сross Site Request Forgery, или, сокращенно, CSRF): что это такое и с чем его едят.

    CSRF — это вид атак на посетителей веб-сайтов, использующий недостатки протокола HTTP. Если жертва заходит на сайт, созданный злоумышленником, от её лица тайно отправляется запрос на другой сервер (например, на сервер платёжной системы), осуществляющий некую вредоносную операцию (например, перевод денег на счёт злоумышленника). Для осуществления данной атаки, жертва должна быть авторизована на том сервере, на который отправляется запрос, и этот запрос не должен требовать какого-либо подтверждения со стороны пользователя.

    Данный тип атак, вопреки распространённому заблуждению, появился достаточно давно: первые теоретические рассуждения появились в 1988 году, а первые уязвимости были обнаружены в 2000 году.

    Одно из применений СSRF — эксплуатация пассивных XSS, обнаруженных на другом сервере. Так же возможны отправка спама от лица жертвы и изменение каких-либо настроек учётных записей на других сайтах(например, секретного вопроса для восстановления пароля).

    Живой пример



    Например, нам нужно сделать небольшой модуль, который должен аяксом удалять ноды. Это можно реализовать служебной ссылкой ноды, при нажатии которой, отправляется аякс запрос на друпаловский путь. К этому пути прицеплен обработчик, который и удаляет ноду. Вот примерно таким модулем все и делается:

    node_destroy.module


    1. /**
    2. * Реализация hook_menu(). Регистрирует наш коллбек в системе меню.
    3. */
    4. function node_destroy_menu() {
    5.   $menu['node/%node/destroy'] = array(
    6.     'page_callback' => 'node_destroy',
    7.     'page_arguments' => array(1),
    8.     'access_arguments' => array('administer nodes'),
    9.     'type' => MENU_CALLBACK,
    10.   );
    11. }
    12.  
    13. /**
    14. * Реализация коллбека.
    15. */
    16. function node_destroy($node) {
    17.   if ($node->nid) {
    18.     node_delete($node->nid);
    19.     print('SUCCESS');
    20.   }
    21.   // в коллбеках для аякса почти всегда надо принудительно завершать скрипт,
    22.   // чтобы не выводить оформление сайта вместе с вашими данными
    23.   exit();
    24. }
    25.  
    26. /**
    27. * Реализация hook_link(). Добавляем свою ссылку в служебные ссылки ноды.
    28. */
    29. function node_destroy_link($type, $node = NULL, $teaser = FALSE) {
    30.   switch ($type) {
    31.     case 'node':
    32.       // если эта функция вызывается, значит мы выводим ссылки ноды,
    33.       // а это значит, что нам и скрипты нужны
    34.       $path = drupal_get_path('module', 'node_destroy');
    35.       drupal_add_js($path .'/node_destroy.js');
    36.  
    37.       // собственно, добавление ссылки
    38.       $links['node_destroy'] = array(
    39.         'title' => t('Destroy node'),
    40.         'href' => "node/$node->nid/destroy",
    41.         'attributes' => array('class' => 'node_destroy_link'),
    42.       );
    43.     break;
    44.   }
    45.   return $links;
    46. }


    node_destroy.js


    1. // Таким нехитрым путем правильно инициализировать некие действия
    2. // вместо обычного $(document).ready(function() { ... })
    3. Drupal.behaviors.node_destroy = function(context) {
    4.   // Мы перебираем все наши ссылочки и навешиваем на них аякс запросы.
    5.   // Заметьте необычный селектор. Он предотвратит двойное навешивание обработчиков.
    6.   $('.node_destroy_link:not(.processed)', context).addClass('processed').click(function(){
    7.       href = $(this).attr('href');
    8.       $.ajax({
    9.         type: "GET",
    10.         url: href,
    11.         success: function(result){
    12.           // SUCCESS нам возвращает наш коллбек меню, если все замечательно
    13.           if (result != 'SUCCESS') {
    14.             alert('Error');
    15.           }
    16.         }
    17.       });
    18.   });
    19. }


    И все бы хорошо, но в один солнечный день, на сайт приходит злой тролль… Или более жизненная ситуация — озлобленный бывший сотрудник приходит на сайт и пытается его поломать. Помня старый опыт, он пробует зайти по адресу site.ru/node/123/destroy, но получает от ворот поворот, так как уже не имеет прав на удаление материалов.

    И тут, в порыве деструктивного креатива, он создает ноду с таким контетом:
    <img src="http://site.ru/node/123/destroy" />

    Что происходит в этот момент? Никакая картинка, естественно, не подгрузится, но браузер тролля выполнит запрос на этот путь с прежним результатом.

    Смирившись с неудачей, тролль уходит с сайта. Через день, администратор сайта замечает эту мусорную ноду, заходит в нее и удаляет. А вернувшись в список материалов, не находит в нем ноды с айдишником 123. Атака удалась. Занавес.

    Для тех, кто не понял, когда администратор зашел в ноду, его браузер тоже ломанулся по ссылке картинки. Но здесь уже прав доступа хватило, и нода была успешно удалена, а админ даже ничего не заметил.


    Как избежать CSRF уязвимостей?



    Ответ — использовать уникальные ссылки для действий по изменению данных. Как это возможно? В друпале используется метод токенизации ссылок. Это означает, что к ссылке активного действия, прибавляется уникальный параметр, который проверяется при осуществлении самого действия. В друпале сгенерировать такой параметр можно функцией drupal_get_token(). Проверить —drupal_valid_token(). Токен генерируется на основе подаваемого значения, сессии пользователя, а также приватного ключа сайта, что практически сводит на ноль вероятность генерации вредителем правильного токена.

    Внесем изменения в наш модуль. Начнем с выставления правильной ссылки:

    1. function node_destroy_link($type, $node = NULL, $teaser = FALSE) {
    2.   switch ($type) {
    3.     case 'node':
    4.       $path = drupal_get_path('module', 'node_destroy');
    5.       drupal_add_js($path .'/node_destroy.js');
    6.  
    7.       $links['node_destroy'] = array(
    8.         'title' => t('Destroy node'),
    9.         'href' => "node/$node->nid/destroy",
    10.         'attributes' => array('class' => 'node_destroy_link'),
    11.         // query — это все GET параметры, т.е. все что в ссылке находится после знака вопроса
    12.         // мы добавляем параметр token
    13.         'query' => 'token='. drupal_get_token('node_destroy_'. $node->nid)
    14.       );
    15.     break;
    16.   }
    17.   return $links;
    18. }



    Как вы помните, мы шлем аякс запрос по адресу, который зашит в ссылке, поэтому в коллбеке нам остается только проверить $_GET стандартным способом.

    1. function node_destroy($node) {
    2.   if ($node->nid && isset($_GET['token']) && drupal_valid_token($_GET['token'], 'node_destroy_'. $node->nid)) {
    3.     node_delete($node->nid);
    4.     print('SUCCESS');
    5.   }
    6.   exit();
    7. }


    via DrupalDance: Подделка межсайтовых запросов
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 37

      0
      упс… так и ajax-отправку тех-же сообщений можно через токены проверять…
      пошел крутить токены на ajax_privatemsg

      «низко кланяемся барину в ножки» (с) не помню.
        +1
        Аякс в общем-то тут вообще для примера :) главное — обработчики.
          0
          я обработчики и имел ввиду. ajax_ это так… выпендреж ;)
        0
        Отличный разбор полётов, пошёл исправлять ошибки. Спасибо
          –2
          Большое спасибо за статью.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              дядя, атата, это плохо. труЪ хацкеры сообщают о найденных уязвимостях. они само благородство. а вот всякие скрипт-киддисы наичинают бесноваться, и крушить все подряд(а о проксях и логах они знают??).
              кстати были недавно XSS уязвимости на яндексе и рамблере кажись… или мэйле. не помню точно. не могу найти=(
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  давайте ссылку — вместе поглядим=) одна голова хорошо. ну а две = хорошо^2
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      я поражен наплевательским отношением к дырам на сайтах контор.
                      сайт алькогольной компании довольно крупной(импортер в Россию). я сообщал об уязвимости(SQL Inj + еще одна с раскрытием путей). закрыли спустя год где-то=) на некоторых до сих пор актуальны(Genser например).
                      про доверие это точно. много каках нынче развелось.
                      • НЛО прилетело и опубликовало эту надпись здесь
            • НЛО прилетело и опубликовало эту надпись здесь
              • НЛО прилетело и опубликовало эту надпись здесь
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    И не только AJAX, а вообще все запросы, изменяющие данные, надо пересылать POST'ом. Это ж элементарные вещи, основы HTTP можно сказать.
                      –1
                      гыгы, а в посте даже фильтровать ничего не надо! его же не видно!
                      +3
                      POST без ключика не безопаснее GET
                        +1
                        по-моему, заставить человека отослать POST на сервер сложнее чем GET. Расскажите про простой способ отсылки POST запрос от жертвы?
                          0
                          форма в которой submit нажимается javascript'ом
                        –1
                        Это не решение. Я могу создать зло-сайт и все приходящие на него будут (к примеру) спамить ваш сайт, если они на нем зарегистрированы, а ваш сайт подверен XSRF.

                        Дешевое решение — проверять реферер (работает только если у вас нет XSS на сайтек, но думаю в 2009 оставлять XSS уже несерьезно), это защизает от атак со сторонних сайтов.
                        Вообще XSRF — следствие просчета при проектировании браузеров и HTML.

                        p.s То что написано — откровенный боян, полгода назад (а может и раньше) вроде как эта уязвимость была вконтактике.
                          0
                          Дешевое решение — проверять реферер

                          чем же оно дешевое такое?
                            0
                            Очень просто реализуется, хотя и низкоэффективно.
                            0
                            > работает только если у вас нет XSS на сайтек, но думаю в 2009 оставлять XSS уже несерьезно

                            Ой, фигню пишу. стработает, если злоумышленник не может размещать на вашем сайте картинки и яваскрипт, вот что я имел в виду.
                          0
                          эта дырка активно года 2 назад обсуждалось… и страдали ей тогда много крупных приложений…
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              на моей памяти только вбуллетин с первых версий проверяет эти данные. даже в админке есть поля для разрешенных сайтах, жаль что с него не брали пример тогда… может просто не знали, зачем эта фишка.
                            –2
                            спасибо, полезно
                              0
                              Не «подделка межсайтовых запросов», а «кузница», если дословно перевести. А если не дословно, то все равно «подделка» и «межсайтовых» вместе не должны быть. Подделываются то обычные запросы, с помощью межсайтовых запросов.
                                0
                                По-моему, это не решение проблемы, а «костыль».
                                Проблема в архитектуре, это неправильно, что действие выполняется при заходе на определённый url, правильнее, как выше написали, все действия выполнять POST-запросом.

                                А при выполнени серьёзных действии, особенно при удалении, не лишним будет выводить предупреждение «Вы уверены, что хотите сделать то-то?».
                                  0
                                  POST подделать не сложнее. С вашей точкой зрения не согласен, она не аргументирована. Token обеспечивает максимальную защиту, вы не подделаете его без доступа к сессии.
                                    0
                                    Как POST подделать?
                                    Если с GET всё понятно, вставить ссылку или картинку с нужным путём, то форму вам вставить никто не даст…
                                      0
                                      Естественно, используя XSS — засабмитив форму джаваскриптом. У нас в условиях не было что сайт не пробиваем для XSS, тем более, мы не застрахованы от полной доступности HTML формата для всех. Думаю, здесь вы со мной согласитесь. И вот в этих условиях Token все-равно остается непробиваем. Т.к. даже получив сессию кого-то из пользователей, вы не сможете сгенерить верный токен без секретного ключа сайта. Конечно, это все уже немного не в ту степь, ибо если вы получили сессии, то все намного проще и по-другому — можно просто залогиниться под пользователем. Но мы ведь рассматриваем только аспект текущей проблемы с CSRF.
                                        0
                                        И вот в этих условиях Token все-равно остается непробиваем.
                                        Если мы можем вставить Javascript, который отправляет форму, то мы можем вставить и скрипт, который найдёт нужную сылку у пользователя, соответственно в которой уже будет token, и запросить её.
                                        Ну и как Вы сами сказли, имея возможность вставлять лобой html и javascript, мы можем сделать куда более интересные вещи, чем просто запросить определённыйй url.

                                        Поэтому я предлагаю не обсуждать «сферического коня в вакууме», а вернуться к реальному положению дел, где разработчик предусмотрел фильтрацию содержимого, и ничего кроме текста и нескольких стандартных тэгов вставить нельзя, никаких форм, никакого яваскрипта.
                                        В таких условиях как подделать POST-запрос?
                                          0
                                          Да никак, собственно также никак, как и поломать токен.
                                            0
                                            Куда-то мы не в ту степь ушли. Я же не говорю, что отправлять POSTом хуже. Я только говорю, что ключ все-равно следует включать, дабы хотя бы попытаться избежать тех маловероятных сферических конев в вакууме. Во всех друпаловских формах ключ включается автоматом. Все действия удаления — требуют подтверждения. Мы сейчас обсуждали мой пример, который на то и пример, чтобы быть очевидным для понимания.
                                              0
                                              Я написал лишь, что если бы изначально не было проблем с архитектурой, то можно было бы вообще обойтись без token'а.

                                              Я считаю, что залог безопасности не в быстром латании дыр, а в тщательном проектировании и грамотной архитектуре изначально, при которой таким дырам просто неоткуда будет взятся.

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

                                Самое читаемое