вторник, 1 ноября 2011 г.

Mojolicious и защита от Cross Site Request Forgery (CSRF)

Немного теории
Я полагаю многие знают, что такое CSRF. Но для новичков озвучу. CSRF (Cross Site Request Forgery) - это межсайтовая подделка запросов. И происходит она следующим образом.

Допустим, на вашем сайте, назовем его "mysite.com", есть ссылка, которая удаляет сообщение. Ссылка вида - "http://mysite.com/myprofile/delete?message=123".

Злоумышленник может вставить эту ссылку на свой сайт "attackerssite.com", например,  в виде картинки -
<img src="http://mysite.com/delete?message=123" />
И если Вы зайдете на "attackerssite.com", то будет отправлен запрос на загрузку картинки с вашего сайта, но вместо загрузки картинки будет удалено сообщение. Проблем с авторизацией не будет, так как будет отправлена ваша кука, запрос же отправляется с вашего браузера.

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

Тогда, ссылка "http://mysite.com/myprofile/delete?message=123" превратится в
"http://mysite.com/myprofile/delete?message=123&secret=af39hf029h3uwe0183y12fnsd". Поскольку "secret" для каждой сессии свой, то злоумышленник его не знает и не может сформировать опасную ссылку.

Способы, которые не работают:
  1. Использование POST. Наивно полагать, что такой вариант поможет. Злоумышленник может без проблем засабмитить форму со своего сайта при помощи JavaScript
  2. Проверка по HTTP Referrer. Этот вариант лучше, но все равно не обеспечивает должного уровня безопасности. Во-первых, HTTP Referrer можно подделать в некоторых версиях Flash, во вторых у вашего сайта могу возникнут проблемы при работе через прокси-сервера.
  3. Проверка по заголовку запроса "X-Requested-With".  Раньше считалось надежным вариантом, но сегодня существуют способы обхода.
Более подробную информацию можно найти в итернетах.

Практика (Mojolicious)
Меня всегда смущало то, что в базовых плагинах Mojolicious нет защиты от CSRF. Я как-то оформил feature request, но Себастьян предложил мне написать плагин самому. Я этим и занялся :). Замему, что на CPAN существовал ( и сейчас существуюет ) плагин  "Mojolicious::Plugin::CSRFDefender", но у него есть ряд недостатков:
  1. Он не защищает при AJAX-запросах. Я запостил реквест автору, но ответа не получил.
  2. Он грубо работает. Этот плагин парсит каждый ответ клиенту и пытается там найти что-то похожее на html-форму. И если, что-то находит, то вставляет туда скрытый параметр.
  3. Защищает только POST запросы
В связи с этим был написан новый плагин "Mojolicious::Plugin::CSRFProtect", лишенный этих недостатков.
Работает он следующим образом:
  1. Подменяет "form_for" хелпер с Mojolicious::Plugin::TagHerlpers на защищенную  версию, которая вставляет в форму скрытый параметр с секретным маркером. Для обеспечения защиты, Вы должны использовать хелпер "form_for".
  2. Добавляет к каждому AJAX запросу заголовок X-CSRF-Token с секретным маркером в качестве значения.(пока работает только для JQuery)
  3. Отклоняет(с 403 ошибкой) все не GET запросы с неправильным секретным маркером
Для того, чтобы включить защиту AJAX-запросов, необходимо в основной шаблон добавить
<%= jquery_ajax_csrf_protection %>
Этот хелпер вставляет следующего рода код:


Дополнительно есть возможность валидировать GET-запросы вручную, например:
В шаблоне:
<a href='/logout?csrftoken=<%= csrftoken %>' >
В контроллере:
if ( $self->is_valid_csrftoken() ) {
#    do logout here
}

16 коммент.:

fuksito комментирует...

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

koorchik комментирует...

Согласен, мне тоже кажется, что защита от CSRF должна идти, как core-плагин. Но увы, пришлось решать эту проблему самому.

Александр комментирует...

Такой сценарий возможен для обхода защиты?
1) Злоумышленник при помощи JavaScript скачивает страницу с формой
2) Достает значение csrftoken (secret) из формы
3) Формирует управляющий запрос используя полученное значение

koorchik комментирует...

2Александр:
Такой сценарий не сработает, поскольку кросс-доменно подгрузить форму при помощи AJAX не получится. Возможно, конечно, подгрузить страницу в iframe, но получить к странице доступ с другого домена тоже не получится.

Обход защиты возможен, если злоумышленнику удастся разместить свой javascript-код на странице атакуемого сайта, но тогда он сможет и cookie без проблем переслать.

afiskon комментирует...

Внезапно плагин перестал сохранять токен в сессию. Точнее, он вызывает Controller::session, но при считывании получает undef и вообще при считывании происходит мистика - весть стэш пустой. Не знаю, куда копать, пришлось пока что отключить плагин.

Mojolicious 2.51 (не лайт)
Mojolicious::Plugin::CSRFProtect 0.08
Mojolicious::Plugin::BasicAuth 0.06
Mojolicious::Plugin::Validator 0.0013

Проверил под Morbo и Starman.

koorchik комментирует...

Очень странно, у меня этот плагин постоянно в использовании и все работает.
Окружение - Mojolicious 2.51(Не лайт) + много разных плагинов.

Возможно поможет очистка cookie.

afiskon комментирует...

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

koorchik комментирует...

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

Сразу можете создать тикет на гитхабе

Sergey Malochinskiy комментирует...

Вопрос.
Если приходит запрос без токена, то генерируется страница "Forbidden!" как это событие перехватывать глобально в приложении?
В каждом контроллере делать проверку не удобно, а вот глобальный callback было бы удобно.

koorchik комментирует...

Запрос без токена - невалидный запрос и его можно просто игнорировать. Я не совсем понимаю зачем его перехватывать?

Sergey Malochinskiy комментирует...

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

koorchik комментирует...

2Sergey Malochinskiy:
Я думал добавить параметр "error_template", но колбек будет более универсален конечно.
Добавлю, наверное, что-то типа:
on_error => sub {
my $c = shift;
# Do what you want here.
}

Один минус колбека - его не описать в конфиг файле.

Sergey Malochinskiy комментирует...

2koorchik:
Спасибо. Буду ждать релиза. :)
Опциональный колбек и шаблон конечно отрисует если надо, а вот из конфигурации да не описать.

koorchik комментирует...

Залил свежую версию на СPAN с поддержкой "on_error" callback.

Sergey Malochinskiy комментирует...

2koorchik:
Потрясающе оперативно.
Я едва успел в TODO запись сделать, а тут уже новая версия. Спасибо!

Skrigger комментирует...

Автор фреймворка прислушался к вам, спасибо.
http://mojolicio.us/perldoc/Mojolicious/Guides/Rendering#Cross-site_request_forgery

Отправить комментарий

Не забудьте добавить себя в постоянные читатели и включить уведомления о новых комментариях, либо воспользуйтесь RSS каналом ;)