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

Главные вкладки

Аватар пользователя neochief neochief 17 февраля 2009 в 0:59


Поводом к написанию этой статьи послужило нахождение мною уязвимости в одном довольно известном модуле. Так как по правилам обнаружения уязвимостей, я пока не вправе распространяться о деталях, то расскажу об уязвимости в общих чертах, а также о методах борьбы с ней. 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

Поддержите на хабре, пожалуйста

Комментарии

Аватар пользователя Stan.Ezersky Stan.Ezersky 17 февраля 2009 в 3:27

Поддержал на Хабре. Спасибо за разбор полётов, вовремя. Я недавно с друпалом и такие вещи в изучении незаменимая вещь. Она помогает избежать дальнейших ошибок. Еще раз спасибо, neochief!

Аватар пользователя VladSavitsky VladSavitsky 17 февраля 2009 в 10:54

Основательная работа. Спасибо. Подшил в раздел "Безопасность".

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

Аватар пользователя astal astal 17 февраля 2009 в 14:12

старые грабли на новый лад.
раньше этим на drupal.ru и на хабре поднимали пузомерки на темы и пользователей, теперь нужно придумывать специальные модули для показа этих проблем?

за вариант обхода данной уязвимости - спасибо, но как заметили эта ошибка у стороннего модуля(пускай возможно и популярного), а на сторонних модулях найти можно как SQL-инъекции и не менее популярные XSS

они были и к сожалению будут появляться новые...

Аватар пользователя Valeratal Valeratal 17 февраля 2009 в 15:20

спасибо, пример очень понравился Smile
поддержал на хабре

а что если, вставить ссылку вида node/номер/delete

сработает?

Аватар пользователя Demimurych Demimurych 18 февраля 2009 в 20:08

Valeratal wrote:
спасибо, пример очень понравился Smile
поддержал на хабре

а что если, вставить ссылку вида node/номер/delete

сработает?

конечно. Только нода не удалится. Потому как по этому пути всегда (если руками не патчить) возвращается форма - да нет

Аватар пользователя neochief neochief 17 февраля 2009 в 15:29

"astal" wrote:
они были и к сожалению будут появляться новые

Главное, чтобы оставались люди, готовые улучшать положение вещей. Причитания точно не помогут, а накрутку можно было пофиксить еще 2 года назад. Чего ж никто не пофиксил?

Аватар пользователя Ветер Ветер 17 февраля 2009 в 16:11

А не проще сделать обработку тега IMG.
Если адрес картинки не оканчивается разрешенным расширением, то запретить запись ноды.

Аватар пользователя igor701 igor701 17 февраля 2009 в 17:10

Хороший пример!

Думаю, выход - передавать в запросе зашифрованное число-сессию вида f(секретный код, IP пользователя, время). Заодно по истечении времени - уничтожать.

А вообще-то достаточно даже просто хэш секретного кода передавать. Главное при инсталляции (друпала или модуля) этот код первый раз генерировать уникальный.

Аватар пользователя neochief neochief 17 февраля 2009 в 17:37

"igor701" wrote:
Думаю, выход - передавать в запросе зашифрованное число-сессию

Почитайте статью, и не выдумывайте велосипед.

Аватар пользователя seaji seaji 17 февраля 2009 в 21:17

"Valeratal" wrote:
а что если, вставить ссылку вида node/номер/delete

Не получится, там нужно подтверждение через нажатие кнопки.

Еще вопрос. А время жизни токена какое?

Аватар пользователя Ветер Ветер 20 февраля 2009 в 2:07

Нда, весело админу будет. Причем удалить ноду можно будет только из admin/content/node
Вариант
- включить тригерры и проверять наличие таких подлостей.

Что там у нас еще без подтверждения работает?:)

Аватар пользователя Demimurych Demimurych 19 марта 2009 в 12:08

хм.

Если следовать неписанному стандарту, то гет запросом можно ТОЛЬКО ПОЛУЧАТЬ информацию, но не модифицировать.

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

Аватар пользователя Demimurych Demimurych 19 марта 2009 в 22:53

"<a href="mailto:fasdalf@fasdalf.ru">fasdalf@fasdalf.ru</a>" wrote:
jquery и post делать умеет.

вы это к чему?

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

Аватар пользователя fasdalf@fasdalf.ru fasdalf@fasdalf.ru 20 марта 2009 в 16:45

нет большой разницы между POST и GET. Post запрос не на много сложнее подделать.
Другое дело, что написанное здесь можно и нужно применять и для обработки POST-форм.

Аватар пользователя Demimurych Demimurych 21 марта 2009 в 14:06

"<a href="mailto:fasdalf@fasdalf.ru">fasdalf@fasdalf.ru</a>" wrote:
нет большой разницы между POST и GET. Post запрос не на много сложнее подделать.
Другое дело, что написанное здесь можно и нужно применять и для обработки POST-форм.

Давайте говорить предметно.
Не на много сложнее говорите?
Живой пример в студию.

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

И так, по клику на кнопке я посылаю ПОСТ запрос который обрабатывает моя функция и удаляет ноду. Я посылаю параметров просто nid ноды. Никаких токенов не использую.

Теперь прошу, опишите мне ситуацию при которой вы бы могли сделать так, чтобы я невольно эту ноду удалил посетив чужую страницу.

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

Аватар пользователя zubr zubr 11 июня 2009 в 8:46

Demimurych wrote:
И так, по клику на кнопке я посылаю ПОСТ запрос который обрабатывает моя функция и удаляет ноду. Я посылаю параметров просто 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 к этой куке)
// (работает начиная с PHP 5.2.0)

setcookie('nameCookie','valueCookie',0,'','',false,true);

echo 

'<b>Реально в куках есть:</b><br>
nameCookie='
.@$_COOKIE['nameCookie'].'; PHPSESSID='.@$_COOKIE['PHPSESSID'];

echo 

'<script>
alert("javaScript прочитал куки:\n"+document.cookie);
</script>'
;

?>

Поэтому, будем считать, что javaScript не может прочитать сессию админа (если админ использует современный браузер).

Тем не менее, через метод POST, без токенов, взломать сайт можно:
Допустим в админке сайта есть визуальный редактор (в нем можно вставлять формы и скрипты) и им пользуются супер админ и помочник у которого права только на правку новостей. Так вот этот помочник, создаст в одной из новостей необходимую форму + javaScript и попросит админа взглянуть на новость, на сайте. В момент просмотра новости, незаметно, будут выполнены любые команды от имени админа. В логах, так же будет написано, например: "админ X удалил ноду Y". Если помочник уберет из новости свой код, то гореадмину будет сложно понять, что происходит.

Имхо, "токены" нужны. Особо нужны в программах PHP, которые: создают аккаунты, изменяют права, менеджеры баз данных, файлов и т.п. Ибо помочник может быть добрым, но безобразно относиться к своему паролю. А если стырят его пароль, то без особого труда испаганят не только доверенные ему новости, но и весь сайт, нахрен.

Аватар пользователя neochief neochief 21 марта 2009 в 17:02

Да ну что вы прицепились к человеку со своим постом. Вам описали решение проблемы в случаях, когда POST использовать не представляется возможным. Джейквери, джейквери.. а если джаваскрипт вырублен и нужно сделать действие БЕЗ формы? Что тогда делать? Везде пихать кнопки и подтверждения? Каким тогда будет интерфейс?

И почему, раз пост такой непробиваемый по-вашему, токены автоматом вставляеются во все друпаловские формы? А я вам отвечу — потому, что это надежнее простого поста.

Аватар пользователя Demimurych Demimurych 21 марта 2009 в 18:35

"neochief" wrote:
И почему, раз пост такой непробиваемый по-вашему, токены автоматом вставляеются во все друпаловские формы? А я вам отвечу — потому, что это надежнее простого поста.

Вы простите ушли от темы разговора.

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

тут пост практически не пробиваем. Будете спорить? Примеры в студию.

Покажите мне реальную ситуацию когда пост использовать нельзя это раз
Расскажите мне почему может быть невозможно использовать форму - это два

А то Ваши рассуждения мне очень напоминают анекдот, про студента который учил все про блох а его спросили про рыб, и он начал свой ответ что если бы у рыб была бы шерсть то в нем были бы блохи. А блохи это...

Не уходите от темы. Мы обсуждаем именно подделку межсайтовых запросов, а не пост или гет в целом.

Зачем городить огород с токенами, если все это решается банальным постом?

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.

Аватар пользователя neochief neochief 22 марта 2009 в 0:35

Я не могу представить вам кода, которым можно взломать грамотно написанную POST форму. Вы это хотели услышать?

В то же время, я могу представить несколько вариантов, где форму использовать неудобно. Главный — если вы хотите упростить некие действия, не добавляя лишних подтверждений. Да, и спасибо вам за ссылку на w3, я уже там был. Это те чуваки, кторых я очень уважаю, но которые же ввели неудобную бокс-модель в xhtml.

Аватар пользователя inc inc 22 марта 2009 в 2:07

Demimurych, отличная идея.

Если взять, к примеру, действие logout, то достаточно ссылку заменить на кнопку формы, отсылающую post-запрос с параметром(input type=hidden).
А кнопку можно стилизовать как угодно, пользователь и не догадается, что это кнопка.
И никаких подтверждений!

Аватар пользователя Demimurych Demimurych 22 марта 2009 в 2:11

"neochief" wrote:
Я не могу представить вам кода, которым можно взломать грамотно написанную POST форму. Вы это хотели услышать?

Нет

"neochief" wrote:
В то же время, я могу представить несколько вариантов, где форму использовать неудобно.

Код мне не обязателен.
Достаточно описания не надуманного (реального) примера, где использование форм имеет ряд существенных недостатков из которых следует что требуется использовать гет запрос.

На всякий случай еще раз повторю задачу.

Вводные, сайт работает не используя javascript ов (потому как если использует, то выбор в сторону пост очевиден не так ли?)

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

Аватар пользователя Demimurych Demimurych 22 марта 2009 в 2:13

"inc" wrote:
Demimurych, отличная идея.

Идея не моя.

Это стандарт де факто для организации подобного рода запросов. Которым пренебрегают разработчики и выдумывают велосипеды в виде использования токенов.

Аватар пользователя Demimurych Demimurych 26 марта 2009 в 23:41

"neochief" wrote:
Обратите внимание на то, кто запостил репорт:
http://drupal.org/node/413938

Он же и патчи сделал, а lut4rp их всего лишь закомитил ;)

Где Ваши примеры того почему следует использовать ГЕТ? Вместо ПОСТА?

Аватар пользователя fasdalf@fasdalf.ru fasdalf@fasdalf.ru 27 марта 2009 в 11:55

Хочу узнать, как текст цитаты связан с выбором post/get

BTW вот вам и пример. Представьте себе галвную страницу с 10+ формами и у каждой 2 сабмита.

Аватар пользователя Demimurych Demimurych 29 марта 2009 в 15:47

"<a href="mailto:fasdalf@fasdalf.ru">fasdalf@fasdalf.ru</a>" wrote:
Хочу узнать, как текст цитаты связан с выбором post/get

BTW вот вам и пример. Представьте себе галвную страницу с 10+ формами и у каждой 2 сабмита.

Текст цитаты связан с человеком, который сказал что покажет и не показал.

"<a href="mailto:fasdalf@fasdalf.ru">fasdalf@fasdalf.ru</a>" wrote:
BTW вот вам и пример. Представьте себе главную страницу с 10+ формами и у каждой 2 сабмита.

Да и что?
Попробуйте, вы увидите что страница от этого не страдает.

Или, если я вас не правильно понял и вас беспокоят именно серые кнопки?

Аватар пользователя fasdalf@fasdalf.ru fasdalf@fasdalf.ru 30 марта 2009 в 16:37

Таки беспокоят. А прямоугольные цветные бесят.
А про 300% прирост html кода на from destination=... я вообще молчу Smile

Для меня тема GET vs POST закрыта.

Аватар пользователя Demimurych Demimurych 31 марта 2009 в 11:46

"neochief" wrote:
Demimurych, вам что-то доказывать бесполезно.

Дружище.
Вы тут доказать ничего не можете.

Я прошу вас примеры вы молчите.

Я дал Вам ссылки на авторитетные источники.

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

Имейте смелость хотя бы написать в своей теме, что есть как минимум еще один способ решить туже саму проблему.

Аватар пользователя Demimurych Demimurych 31 марта 2009 в 12:14

"seaji" wrote:
Ага, кто то любит кулаками махать, а кто то просто делает полезное дело :)

Ну что решили хуями меряться?
Посмотрели бы секьюрити рассылки внимательнее что ли. Ну или бы ник поугуглили.
Какое это имеет отношение к делу?

Кто то делает дело и молчит.

А кто то размножает НЕПРАВИЛЬНЫЕ способы работы с подобного рода задачами.

Аватар пользователя Demimurych Demimurych 31 марта 2009 в 11:44

"<a href="mailto:fasdalf@fasdalf.ru">fasdalf@fasdalf.ru</a>" wrote:
Таки беспокоят. А прямоугольные цветные бесят.
А про 300% прирост html кода на from destination=... я вообще молчу Smile

Для меня тема GET vs POST закрыта.

1. Вы что еще не используете gzip компрессию? Сравните сжатый код страницы в 100 килобайт и страницы в 50 килобайт.

2. Кнопки не обязательно должны выглядеть так как они выглядят по умолчанию. Они могут выглядеть так, что вы никогда не помете что это кнопка пока не посмотрите в код. htmlbook.ru в руки

Аватар пользователя fasdalf@fasdalf.ru fasdalf@fasdalf.ru 31 марта 2009 в 14:45

"типичный пользователь" может и не отличит. Он и jpg может за кнопку принять. Я - отличу. А htmlbook читать второй раз не собираюсь.
Засим прощайте.

Аватар пользователя seaji seaji 31 марта 2009 в 17:36

to Demimurych: Слушайте, ни кто не собирается с Вами ни чем мериться.
Речь идет о том, что вот Вы не решили эту задачу, а орете и хамите больше всех.
И абсолютно не важно "хорошее" или "плохое" это было решение.
Это АБСОЛЮТНО не важно.
Понимаете, лучше пусть будет плохое решение, чем не будет хорошего решения.
Так что Вы сначала сделайте "хорошо", а уж потом мы это и обсудим.