Защита от темных сил: Межсайтовая подделка запроса

В этой статье, в стилизации под Гарри Поттера, описана одна из самых распространенных проблем, а именно, межсайтовая подделка запроса.

Чтение данного материала как поможет устранить проблему, так и доставит удовольствие.

После неопределенного «нападения оборотня» мы стали новыми управляющими веб-приложения hogwarts.edu.

Наш первый день начался со встречи с профессором Дамблдором, который, приближаясь к нам, объяснял, что с его официального аккаунта hogwarts.edu стали, иногда, приходить мистические сообщения всем студентам такого рода: «Поттер неудачник, Малфой лучший».

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

1. Аутентификация

дамблдор

Первое что мы сделаем – это посмотрим на серверный код, который отвечает за отправку сообщений. Он очень простой. Вот что этот код делает:

  1. Ожидает http-запрос "hogwarts.edu/dumbledore/send-message?to=all_students&msg=blahblah"
  2. Отправляет “blahblah” (независимо от того, что было установлено в параметр msg) от @dumbledore всем студентам.

Как мы видим, здесь нет проверки на то, действительно ли отправителем сообщения является Дамблдор. Это означает, что любой злоумышленник может отправить http-запрос на hogwarts.edu/dumbledore/send-message и он будет  обработан как обычный запрос. Возможно, наш предшественник оборотня думал, что все будет хорошо.

Для предотвращения подобного в будущем, мы вводим систему аутентификации.

Во-первых, мы добавляем Секретный Ключ Аутентификации для каждого пользователя, который рандомно генерируется, когда пользователь заходит в систему, после чего он удаляется, когда пользователь выходит из системы.

Также мы слышали о том, что куки имеют проблемы с безопасностью, поэтому их не используем. Вместо этого, при заходе пользователя в систему, сгенерированный секретный ключ мы записываем в localstorage, и при помощи JavaScript кода включаем этот ключ в качестве заголовка под названием “secret-authentication-key” в наши неподдельные http-запросы.

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

  1. Ожидаем http-запрос "hogwarts.edu/dumbledore/send-message?to=all_students&msg=blahblah";
  2. Проверяем заголовок с названием “secret-authentication-key” и убеждаемся, что он соотносится с Секретным Ключом Аутентификации, который мы записывали в базу аккаунта Дамблдора, если ключ неверный, то отклоняем данный запрос;
  3. Отправляем “blahblah” (и все что находится после параметра msg) от @dumbledore  всем студентам.

Теперь при попытке отправить поддельные сообщения от лица Дамблдора, сервер отклонит их из-за отсутствия надлежащего ключа аутентификации. Зато, когда Дамблдор зайдет в систему и попробует отправить сообщение студентам, то все будет работать. Ура!

2. Куки

снейп

После того, как мы запустили новую систему аутентификации, профессор Снейп трансгрессировал с жалобой.

После того, как он посетил hogwarts.edu/snape/messages  для того, чтобы увидеть свои приватные сообщения, то перед тем как сообщения загрузились, на экране появился небольшой значок загрузки в виде спиннера. Снейп потребовал, чтобы сообщения, как раньше, загружались мгновенно.

Почему мы добавили загружающийся спиннер? Так, как уже было сказано hogwarts.edu/snape/messages были не безопасны, поэтому мы обеспечили безопасность путем добавление нашего нового “secret-authentication-key” в заголовок http-запроса.

Теперь проблема заключается в том, что когда профессор Снейп заходит на hogwarts.edu/snape/messages  браузер не знает как отправить такой необычный заголовок в изначальный http-запрос к серверу. Вместо этого сервер отправляет обратно HTML, содержащий загрузочный спиннер и некоторый JavaScript код. JavaScript считывает ключ из localStorage и создает второй запрос (в этот момент в заголовок встраивается наш “секретный ключ аутентификации”), который окончательно позволяет получить сообщения профессора Снейпа от сервера.

Пока второй запрос находится в обработке, все внимание профессора Снейпа направлено на этот, вызывающий гнев, спиннер.

Мы исправили проблему юзабилити путем замены нашего заголовка, содержащего “секретный ключ аутентификации”, на “Cookie” заголовок. Теперь, при регистрации профессора Снейпа, мы, для хранения ключей, localstorage не используем, как и любой JavaScript-код. Вместо этого наш сервер при ответе вставляет в заголовок: "Set-Cookie: key_info_goes_here". Браузер знает, что наличие заголовка Set-Cookie в HTTP-ответе означает, что он должен локально сохранить ключ на устройство Снейпа в виде куки.

В результате, откуда бы браузер Снейпа не заходил (http-запрос) на hogwarts.edu, он автоматически отправит содержимое файла куки в заголовок Cookie. Это работает даже для первоначального HTTP GET-запроса, который осуществляется при заходе Снейпа на hogwarts.edu/snape/messages – означает, что теперь наш сервер может аутентифицировать его верно при первом же запросе и отправлять сообщение в первом ответе, не нуждаясь во втором HTTP-запросе.

Вот наш новый процесс:

  1. Ожидаем http-запрос "hogwarts.edu/dumbledore/send-message?to=all_students&msg=blahblah";
  2. Проверяем заголовок с названием “Cookie”, убеждаемся, что он совпадает с нашим “секретным ключом аутентификации”, который мы храним в базе данных для аккаунта @mcgonagall. Если заголовок не совпадет, то отклоняем запрос.
  3. Отправляем “blahblah” (и все что находится после параметра msg) от @ mcgonagall  всем студентам.

Появившееся проблема решена!

3. Уязвимости Cookie при выполнении GET-запросов

макгонагл

Есть ли причины не использовать куки в первую очередь? Да точно! Безопасность.

И действительно через день после того, как мы внедрили решение, основанное на использовании куки, профессор МакГонагалл пришла со странной историей. Непосредственно, после посещения блога Драко Малфоя, от имени её аккаунта hogwarts.edu была произведена рассылка всем студентам все того же зловредного письма: «Поттер чмо, Малфой лучший». Но как это могло произойти?

Несмотря на то, что куки решили появившуюся проблему, данное решение приоткрыло брешь для нового вектора атак, а именно для Межсайтовой Подделки Запроса или CSRF-атаки.

Просматривая исходный html-код блога Драко, мы заметили это:

<img src="http://hogwarts.edu/mcgonagall/send-message?to=all_students&msg=Potter_sux">

Как только профессор МакГонагалл посетила его блог, её браузер вел себя как обычно при работе с тегом <img>: отсылал HTTP GET-запрос на определенный URL, указанный в атрибуте src этого тега. В силу того, что браузер отправляет данный запрос на hogwarts.edu, то данные аутентификации профессора МакГонагалл, хранящиеся заголовке куки, автоматически включаются в запрос. Наш сервер смотрит совпадают ли куки (они конечно же совпадают) и покорно отправляет зловредное сообщение.

Проклятье!

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

Мы можем исправить это, добавив новый второй шаг в наш список:

  1. Прослушиваем HTTP-запрос "hogwarts.edu/mcgonagall/send-message?to=all_students&msg=blahblah"
  2. Если запрос не является POST-запросом, то отклоняем его;
  3. Проверяем заголовок с названием «Cookie» и удостоверяемся, что он совпадает с нашим Секретным Ключом Аутентификации, который мы храним в нашей базе данных, для аккаунта МакГонагалл. Если заголовок не совпадет, то отклоняем запрос;
  4. Отправляем "blahblah" (или все что находится в параметре msg) с аккаунта @mcgonagall всем студентам.

Отлично! Теперь CSRF-атаки никак не воздействуют на тег <img>, так как этот тег при GET-запросах просто загружает атрибут src. Сейчас профессор МакГонагалл имеет возможность спокойно посещать блог Драко, и не думать о каких-либо проблемах.

4. Уязвимости Cookie при выполнении POST-запросов

малфой

К сожалению, через несколько дней Драко нашел выход из ситуации. Он заменил тег <img> на следующую форму:

<form method="POST" action="http://hogwarts.edu/mcgonagall/
send-message?to=all_students&msg=Potter_sux">

Также он встроил некторый JavaScript-код, который отправляет эту форму, как только страница загружается. И снова, когда профессор МакГонагалл заходит на страницу, то её браузер подтверждает отправку формы, в результате этого создается HTTP POST-запрос, который автоматически подтягивает куки и наш сервер вновь отправляет зловредное сообщение.

Черт побери!

В попытке все немного усложнить, мы изменили параметры msg и to URL-запроса, для того чтобы запрашиваемая информация была отправлена посредством JSON. Это решило проблему на несколько дней, но Драко быстро учится и помещает JSON в <input type = "hidden> внутри формы. Мы снова вернулись к прошлой ситуации.

Мы думаем изменить конечную точку вместо POST будет PUT, поскольку <form> поддерживает только POST и GET запросы, но если рассматривать PUT вместо POST только семантически, то в этом есть смысл. Мы пробовали улучшить до HTTPS (это не помогло) и использовать так называемые «безопасные куки» (так же не помогло) и в тоге остановились на списке подходов, которые не помогают разрешить данную проблему, расположенном на портале OWASP, перед тем как нашли рабочее решение.

5. Усиление политики единого домена

гермиона

OWASP имеет несколько довольно понятных рекомендаций, как защитить себя от, возобновившихся, CSRF-атак. Наиболее надежной формой защиты является проверка на то, что код, отвечающий за отправку запроса был запущен на странице hogwarts.edu.

В тот момент, когда браузер отправляет HTTP-запросы, они содержат в себе, как минимум один (возможно, что оба, это зависит от того является ли данный запрос HTTPS-запросом и насколько устаревшим является браузер) из этих заголовков: Referer и Origin.

Условие

Если HTTP-запрос был создан, когда пользователь находился на hogwarts.edu, то заголовки Referer и Origin будут начинаться с https://hogwarts.edu . Если же запрос был создан, когда пользователь просматривал другую страницу (не hogwarts.edu), например, блог Драко, то браузер без особых вопросов установит данные заголовки (Referer и Origin) к домену блога, а не к hogwarts.edu.

Однако, если мы потребуем, чтобы заголовки Referer и  Origin устанавливались на hogwarts.edu, то в таком случае мы сможешь отклонить все HTTP-запросы, сгенерированные блогом Драко (или любыми другими сторонними сайтами), которые представляют собой угрозу.

Давайте добавим эту проверку в наш алгоритм:

  1. Прослушиваем HTTP-запрос "hogwarts.edu/mcgonagall/send-message";
  2. Если запрос не является POST-запросом, то отклоняем его;
  3. Если заголовок Origin и/или Referer присутствуют, то проверяем на их принадлежность к hogwarts.edu. При отсутствии обоих заголовков, по рекомендациям OWASP, предполагаем, что данный запрос является угрозой и отклоняем его;
  4. Проверяем заголовок с названием “Cookie” и убеждаемся, что он соответствует Секретному Коду Аутентификации, который мы записывали в базу данных для аккаунта @mcgonagall. Если соответствия нет, то отклоняем запрос;
  5. Отправляем сообщение с аккаунта @mcgonagall, опираясь на JSON в теле запроса.

Отлично! Теперь, если запрос приходит вне браузера и у него нету необходимого заголовка “Cookie”, и если запрос приходит из браузера по средством зловредного блога Драко Малфоя, то он просто не пройдет проверку наличия заголовков Referer и Origin.

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

Например, если мы применим данную политику ко всем GET-запросам, то ни один из них не сможет перейти на hogwarts.edu c другого сайта, так как все они будут отклонены из-за различных Referer-заголовков! А мы только хотим сделать эту проверку (проверку единого домена) для определенных сайтов, чтобы ни один из них не смог получить доступ к hogwarts.edu извне.

Поэтому очень важно, чтобы GET-запросы, в основном, были “только на чтение”, чтобы мы в любой момент могли пропустить проверку на единый домен. Однако старые трюки (использование тега <img>) Драко могут вызвать проблемы в логике нашей проверки. Но благодаря улучшению нашей проверки единственное что будет, при попытке Драко «украсть» информацию, это поврежденные картинки. В тоже время если запрос придет с текущего аккаунта пользователя, то это будет означать что злоумышленник пытается произвести CSRF-атаку с этого аккаунта!

6. Вторая линия обороны

отряд дамблдора

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

Еще одной хорошей причиной делать дополнительную проверку являются внезапно возникающие ошибки (баги) в браузерах. Иногда подобные баги приводят к новым уязвимостям, которыми злоумышленник может воспользоваться, а также возможно, что кто-то сможет раскрыть данную уязвимость в одном из популярных браузеров, тем самым позволяя им подделывать заголовки Origin и Referer.

Обоснование

Наличие второй линии обороны означает, что если наша первая линия обороны не справится, то у нас есть уже развернутая резервная защита.

Проще всего реализовать рекомендованные OWASP дополнительные меры защиты - это использование кастомных заголовков в запросах. Далее показано как они работают.

Когда браузер отправляет HTTP-запросы, используя XMLHttpRequest (также известный как XHR или AJAX- запрос), то они вынуждены подчиняться командам Политики Единого Домена. Для сравнения, HTTP-запросы, отправляемые через теги <form>, <img> и другие элементы, не имеют подобных ограничений. Это означает, что даже если Драко сможет поместит в свой блог <form>, которая будет отправлять HTTP-запрос на hogwarts.edu, то он не сможет использовать XHR в своем блоге для отправки запросов на hogwarts.edu (Другими словами, если мы явно не настроим hogwarts.edu на          совместное использование ресурсов между разными источниками, то само собой это не будет доступно никому).

Отлично! Итак, мы знаем, что если запрос пришел через XHR, а не через <form> или <img>, то он точно был сгенерирован на hogwarts.edu (конечно же, при условии верного заголовка Cookie), вне зависимости от значений заголовков Origin или Referer.

По умолчанию нет способа определения того пришел запрос через XHR или нет. POST-запрос осуществлённый через базовый (без дополнительных настроек) XHR ничем не отличается от POST-запроса, отправленного с помощью <form>. Однако XHR поддерживает возможность, которой нету у <form>: Настройка пользовательских заголовков.

В настройках нашего XHR устанавливаем заголовок: «Content-Type: application/json" (который для нас является семантически правильным, для независимой отправки, так как сейчас мы отправляем JSON), получается, что мы создает HTTP-запрос, который обычная <form> создать не может. Если наш сервер будет проверять наш заголовок ("Content-Type: application/json"), то этого будет достаточно, чтобы быть уверенным, что запрос пришел через XHR. Если же запрос приходит пришел через XHR, то он также должен соответствовать политики единого доступа и, следовательно, он должен приходить со страницы hogwarts.edu!

Вторая линия обороны получается лучше первой так как её можно обойти с помощью Flash. Поэтому, нам, определенно, нельзя пропускать проверку политики единого домена   ! Мы должны использовать данную проверку только в качестве дополнительного уровня защиты от возможных уязвимостей, связанных с заголовками Origin и/или Referer.

Окончательный процесс

поттер

Вот наш окончательный процесс на стороне сервера:

  1. Прослушиваем HTTP-запрос "hogwarts.edu/mcgonagall/send-message";
  2. Если запрос не является POST-запросом, то отклоняем его;
  3. Если заголовок Origin и/или Referer присутствуют, то проверяем на их принадлежность к hogwarts.edu. При отсутствии обоих заголовков, предполагаем, что данный запрос является угрозой и отклоняем его;
  4. Проверяем заголовок с названием “Content-Type”, и убеждаемся, что он установлен в application/json;
  5. Проверяем заголовок с названием “Cookie” и убеждаемся, что он соответствует Секретному Коду Аутентификации, который мы записывали в базу данных для аккаунта @mcgonagall. Если соответствия нет, то отклоняем запрос;
  6. Отправляем сообщение с аккаунта @mcgonagall, опираясь на JSON в теле запроса.

Данный алгоритм действий решает наши текущие проблемы, однако стоит помнить о наших будущих потребностях:

  • Если когда-нибудь мы захотим самостоятельно использовать <form> (вместо XHR), и при этом иметь вторую линию обороны для все той же проверки на единый домен, то мы можем использовать токен синхронизации;
  • Если мы хотим продолжать использовать XHR, но при этом не устанавливать пользовательский заголовок (наподобие Content-Type), или использовать токен синхронизации, то мы можем использовать двойную отправку куки или вместо этого использовать зашифрованный токен;
  • Если мы захотим поддержку CORS (совместное использование ресурсов между разными источниками), ну… тогда нам придется полностью переосмыслить наш подход к аутентификации!

Заключение

Hogwarts.edu теперь в гораздо лучшей форме. Ниже приведен список того, что было сделано:

  1. Была введена система аутентификации, для предотвращения атак путем подмены истинного пользователя;
  2. Были использованы куки, чтобы избежать двух циклических HTTP-запросов (с загрузочным спиннером между ними), для просмотра личной информации, например, личные сообщения пользователей;
  3. Была сделана защита от <img src = "some-endpoint-here"> GET CSRF-атак, путем требования от конечных точек, которые могу вносить изменения, использовать HTTP-операции вместо GET-запросов. (В нашем случае мы использовали POST-запросы);
  4. Была сделана защита от <form> POST CSRF-атак путем проверки заголовков Origin и/или Referer на их принадлежность к hogwarts.edu. (При отсутствии обоих заголовков, предполагаем, что данный запрос является угрозой и отклоняем его);
  5. Была добавлена вторая линия обороны для защиты от потенциальных угроз, связанных с заголовками Origin и/или Referer, путем требования установки заголовка Content-Type в application/json.

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

Ссылка на оригинальную статью
Перевод: Александр Давыдов

Другие статьи по теме

Каталог полезных ссылок для взлома и Reverse Engineering

Серия коротких видео об уязвимости, взломе и способах защиты

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ