Статьи

Как предотвратить повторные атаки на вашем сайте

Предотвращение повторных атак

Эта статья была первоначально опубликована на сайте Tech ‘s Talks Бена.

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

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

Это далеко не полное решение. У него есть недостатки и нерешенные проблемы, но он дает вам общее представление о том, как токены и простые протоколы могут повысить безопасность ваших веб-сайтов. Примеры кода и его реализация выполняются в ASP.NET и C #, но концепция может быть развернута на любой другой платформе или языке программирования.

Концепция одноразовых токенов

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

  1. Клиент отправляет запрос GET, набирая URL или страницу или нажимая на ссылку.
  2. Сервер генерирует случайный токен. Впоследствии он сохраняет копию токена в сеансе и встраивает копию токена в тег <form> ответа, который он отправляет клиенту.
  3. Клиент обрабатывает контент и отправляет POST-запрос на сервер, например, когда пользователь нажимает кнопку, которая содержит случайно сгенерированный токен.
  4. Сервер получает запрос и приступает к его обработке, только если присоединенный токен равен тому, который хранится в сеансе пользователя.
  5. Сервер делает недействительным токен и возвращается к шагу 2, где он формулирует ответ с новым случайным токеном.

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

Тест-кровать

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

простое текстовое поле и код кнопки

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

время подачи плюс данные, содержащиеся в текстовом поле

Это вывод страницы после начального запроса GET

вывод после начального запроса GET

После отправки страницы результат будет выглядеть следующим образом:

вывод после подачи

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

Предотвращение повторных атак

Решение

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

добавление скрытого поля

Далее мы создадим функцию, которая генерирует случайный токен и встраивает его как в скрытое поле, так и в коллекцию сеансов.

случайный токен

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

изменить функцию Page_Load ()

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

переопределить функцию OnPreRender ()

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

ошибка из-за неверного токена

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

Уточнение кода

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

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

Как фанатичный энтузиаст объектно-ориентированного программирования (ООП), я всегда ищу возможности реорганизовать и усовершенствовать код, используя возможности этой самой удивительной парадигмы программирования.

Чтобы решить вышеупомянутые проблемы, первое, что мы делаем, это определяем класс, который будет инкапсулировать функциональность генерации токенов. Мы назовем класс TokenizedPage и выведем его из System.Web.UI.Page, чтобы иметь возможность использовать его для страниц в будущем.

вызывая класс TokenizedPage

Затем, чтобы сделать код более читабельным и управляемым, мы инкапсулируем маркер страницы и маркер сеанса в два разных свойства, которые мы добавляем в класс TokenizedPage. Чтобы сделать код легко переносимым на веб-страницах, мы будем использовать коллекцию ViewState вместо скрытого поля ввода для хранения токена страницы. Мы также используем свойство Page.Title в качестве ключа для хранения токена в сеансе. Это улучшит наш код и частично решит вторую проблему, которая ограничит использование нашего сайта одной вкладкой в ​​браузере. Применяя это изменение, мы сможем открывать отдельные страницы сайта на разных вкладках, но мы не сможем открыть несколько экземпляров одной и той же страницы на отдельных вкладках, потому что они по-прежнему будут обмениваться токенами. , Эта проблема будет решена позже.

инкапсуляция страницы и токенов сессии

Затем мы добавляем доступное только для чтения логическое свойство с именем IsTokenValid, которое следует примеру других свойств страницы, таких как IsPostBack и IsValid. Цель этого свойства — убедиться, что токен страницы равен токену сеанса.

добавление IsTokenValid

Наконец, мы добавляем функцию GenerateRandomToken () и переопределение события OnPreRender (), как это было сделано в тестовом стенде.

GenerateRandomToken и OnPreRender

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

IsTokenValid

Намного лучше.

Делая это еще лучше

Одна из проблем этого кода заключается в том, что если в вашем браузере есть две вкладки, указывающие на одну и ту же страницу, публикация одной из них приведет к аннулированию токена другой, поскольку они используют один и тот же ключ токена сеанса. Эту проблему можно решить, добавив идентификатор токена, который гарантирует, что каждая последовательность запрос-ответ, происходящая на одной вкладке, будет использовать свой собственный набор уникальных токенов и не будет мешать другим запросам на той же странице. Первым делом стоит вернуться к классу TokenizedPage и добавить свойство TokenID. Это свойство генерирует случайный идентификатор при первом вызове в начальном запросе GET и сохраняет его в коллекции ViewState для повторного использования в будущем.

добавление свойства TokenID

Далее мы изменим свойство SessionHiddenToken для использования свойства TokenId вместо использования свойства Page.Title.

используя свойство TokenId

Круто то, что, поскольку мы использовали принципы абстрагирования и инкапсуляции (еще один большой знак преимуществ ООП), нам не нужно вносить никаких других изменений, и новый механизм будет работать со всеми страницами, которые мы извлекли из TokenizedPage.

Остальные проблемы

Это об одном шаблоне токенов. Осталось две проблемы:

  • Точнее, будет генерироваться неограниченное количество идентификаторов токенов для каждого сеанса (количество запросов GET, отправляемых каждому сеансу). Эту проблему можно решить, внедрив механизм стека или кэширования, который выскакивает старые идентификаторы при превышении предела числа или когда они не используются в течение определенного периода времени. Я оставлю реализацию для вас.
  • Генератор случайных чисел по умолчанию — это не то, что вы бы назвали самым безопасным и надежным источником случайности, и опытный хакер мог бы предсказать последовательность токенов. Однако, если вы используете шифрование SSL, они все равно не смогут получить токен.

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