Выявление и предотвращение использования нескольких окон или закладок браузера в веб приложениях

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

Введение

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

“Ограничивайте пользователя одним окном браузера”

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

Проблемы

Есть две проблемы в том, каким образом запретить использование нескольких окон или вкладок:

    1. Каким образом мы можем отличить два запроса (GET or POST) на стороне сервера и узнать пришел ли запрос из того же самого окна или из новой вкладки?

Ниже приведен пример запроса от HTTP клиента к HTTP серверу, работающему на http://www.example.com port 80.

GET /index.html HTTP/1.1
Host: www.example.com

2. Как остановить пользователя, который пытается инициировать новый запрос (GET or POST) из двух разных окон на стороне клиента?

      Ниже приведен пример действий, которые пользователь может выполнить для создания нового окна или вкладки браузера:
Ctrl+N / Ctrl+T
File->New Window / File->New Tab

Замечание: Все окна открытые с использованием указанных выше команд разделяют одну и ту же сессию/cookies в процессе общения с сервером. При создании новой вкладки/окна браузер использует точную копию исходной страницы.

Решение для определения и предотвращения использования нескольких вкладок или окон веб браузера при работе с веб приложением

Есть два подхода, которые мы можем использовать в веб приложении:

  1. Использование window.name на стороне клиента в JavaScript коде
  2. Проверка HTTP Referer header на стороне сервера

Вкратце

Default.aspx открывает Home.aspx устанавливая уникальное значение window.name используя для этого JavaScript. Каждый раз когда страница открывается в другой вкладке/окне скрипт проверяет свойство window.name и перенаправляет запрос на InvalidAccess.aspx если находит некорректное имя.

Подробно, шаг за шагом:

Сначала запрашивается страница Default.aspx.

По событию page load выполняется следующий скрипт:

<script language="javascript" type="text/javascript">
if (window.name == "default") {
    var windowFeatures ='channelmode=0, directories=0,
        location=1, menubar=0,
        resizable=1, scrollbars=1,status=1,titlebar=0,
        toolbar=0,top=0,left=0, width=1010,height=550';
    window.open("Home.aspx", "<%=GetWindowName()%>");
    window.opener = top;
    window.close();
}
else if (window.name == "") {
    window.name = "default";
    window.open("Default.aspx", "_self");
}
else if (window.name == "invalidAccess") {
    //alert("Invalid access. Please close
    //the window, and try again.");
    window.close();
}
else {
    window.name = "invalidAccess";
    window.open("Default.aspx", "_self");
}
</script>

В Default.aspx.cs у нас есть функиця GetWindowName() которая возвращает уникальное имя.

public string GetWindowName()
{
  Session["WindowName"] =
    Guid.NewGuid().ToString().Replace("-", "");
  return Session["WindowName"].ToString();
}

Когда страница загружается первый раз  window.name у Default.aspx содержит пустое значение (“”). Скрипт устанавливает  window.name в значение “default” и открывает себя в том же самом окне заново. Теперь уникальное имя окна установлено (используя функцию на стороне сервера GetWindowName()) и открыта страница Home.aspx.

Скрипт закрывает Default.aspx сразу после открытия Home.aspx.

Замечание: Каждый раз при открытии нового окна значение window.name пустое (“”). Если window.name пустое то Internet Explorer 7 и выше выдает сообщение о подтверждении:

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

На всех остальных страницах (за исключением Default.aspx), мы используем такой скрипт в секции  Head.

<script language="javascript" type="text/javascript">
if(window.name != "<%=GetWindowName()%>")
{
  window.name = "invalidAccess";
  window.open("InvalidAccess.aspx", "_self");
}
</script>

И такой код в соответствующем code-behind (Home.aspx.cs):

public string GetWindowName()
{
  return Session["WindowName"].ToString();
}

2. Проверка HTTP Referer header на стороне сервера

Referer

Это опциональное поле в header позволяет клиенту определить URL адрес документа (или элемента в документе) из которого был выполнен запрос.

Это позволяет серверу создать список обратных ссылок для разных его нужд. И позволяет отследить «плохие» ссылки.

Пример:

Referer: http://www.w3.org/hypertext/DataSources/Overview.html

Если запрос не имеет поля referer это означает что запрос не был сгенерирован используя такие опции:

Ctrl + N/Ctrl + T + K & File->New Window / File->New Tab

(Замечание: При открытии страницы в новом окне или вкладке используя среднюю или правую кнопки мыши значение UrlReferer будет доступно. Но window.name будет пустым. И это условие будет обработано на стороне клиента в скрипте, который мы уже добавили на страницу)

protected void Page_Load(object sender, EventArgs e)
{
  if (Request.UrlReferrer == null)
  {
    //UrlRererrer not found
    Response.Redirect("~/InvalidAccess.aspx");
  }
}

Заключение

Описанное решение позволяет запретить пользователю использование более чем одного окна браузера. Однако стоит заметить что все таки не существует 100% надежно работающего способа обнаружить и предотвратить использования нескольких окон браузера в рамках одного приложения.

Выявление и предотвращение использования нескольких окон или закладок браузера в веб приложениях: 9 комментариев

  1. Rel

    Объясните мне, зачем так делать?

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

    Меня, лично, всегда бесят такие ограничения.

    1. Дмитрий Васильев Автор записи

      Основная проблема в том, что нажимаю Ctrl-N пользователь получает точную копию открытого в данный момент окна.
      А в этом окне пользователь оказался на каком то шаге процесса. Скажем, нужно последовательно провести пользователя через 3 шага.
      Причем он уже находится на втором шаге. Нажал пользователь CTRL-N и мы получили еще одну копию окна на втором шаге и никак не можем отличить эти два окна друг от друга и вся последовательность шагов рушится если пользователь начинает переключаться между окнами и нажимать «продолжить»

  2. Denis Gladkikh

    Жесть какая… Народ не делайте никогда так!!! Что WindowName, что UrlRererrer можно так же с легкостью подменить, если кому-нибудь нужно будет.

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

    “Ограничивайте пользователя одним окном браузера” — откуда это правило, прям интересно?

    Подмена сессии — вы нисколько не решаете эту проблему
    множественные запросы-ответы — тоже самое, как вы решили эту проблему? (если я понимаю это DDoS атакой)
    конкуренция данных — скорее всего хреновая архитектура приложения

    1. Дмитрий Васильев Автор записи

      Если очень хочется ограничить — вот готовое решение 🙂
      А вообще, если пользователь открыл форму редактирования какого либо объекта и «склонировал» страницу в другую вкладку браузера. Нажал на первой вкладке «сохранить», потом на второй тоже. Какая вкладка должна «победить» при сохранении данных? Что станет с какими то «промежуточными» данными, которые нужны при сохранении страницы и уже поменялись ранее при нажатии «сохранить» на первой странице?
      Это же не просто конкурентное сохранение данных, у пользователя точная копия первой страницы и мы не можем отличить ее от оригинала.
      В HTML 5 для решения этой проблемы можно использовать хранилище на стороне клиента, которое НЕ КЛОНИРУЕТСЯ при создании копии страницы.
      И это сделано как раз для решения описанной проблемы. Подскажите, какое архитектурное решение позволит мне склонировать страницу и продолжить работу уже на двух абсолютно идентичных страницах без разрушения контекста и я буду вам очень признателен!

  3. Vadim

    Ну давайте рассмотрим примеры.

    1. Про редактирование формы:
    Допустим мы на форме редактирования пользователя, заполняем поля.
    Клонируем страницу.
    На первой странице жмем Submit.
    Если все хорошо у нас происходит Update в базе данных.
    На второй странице жмем Submit.
    Если все хорошо у нас происходит еще один Update в базе данных.
    Итого: Вставлены два пользователя.
    Я щитаю такое поведение абсолютно логичным.

    И это уже проблемы пользователя, что он один и тот же объект дважды обновил.

    1. Дмитрий Васильев Автор записи

      Хорошо, если задача такая, как вы описали то конечно проблем нет вообще и система может вести себя точно так как вы описали.
      А теперь предположим что у нас ситуация немного другая (приведу сначала пример, который представили разработчики стандарта HTML5):
      Пользователь находится на сайте продажи билетов на самолет и открыл две вкладки (склонировал одну из страниц в другую вкладку). Так как пользователь может переключаться из одной вкладки в другую и переходить с одной вкладки на другую может возникнуть ситуация, когда он случайно или намеренно закроет одну из вкладок и видя вторую будет полностью уверен что билет он не купил. Потенциально это может привести к тому, что пользователь купит два билета вместо одного … Если бы у нас был механизм который позволит определять что вкладка была «склонирована» мы могли бы выдать пользователю предупреждение или выполнить какие то другие действия, которые предотвратят возможную ошибку в действиях пользователя.
      Или другой пример: у нас есть многошаговый визард, состояние объекта, который обрабатывает этот визард хранится где то в сессии с привязкой к конкретной странице. Если пользователь склонировал страницу мы получим две неотличимых друг от друга страницы, которые работают с одним и тем же объектом одновременно и одновременно меняют его части. При сохранении данных с одной из страниц мы можем потенциально получить некорректный объект в памяти. Чтобы не допустить этого можно проверять реальное состояние объекта и то, которое ожидалось при отправке запроса со страницы. Но опять таки если объект очень сложный и в нем очень много полей такая проверка может оказаться непростой задачей. В этом случае может иметь смысл запретить открытие нескольких вкладок для таких критичных страниц.

  4. Vadim

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

    Про заказ билетов: вы пишете «когда он случайно или намеренно закроет одну из вкладок и видя вторую будет полностью уверен что билет он не купил».
    У нас например оплата проходит через «корзину» куда пользователь добавляет различные Items. И когда он на одной из вкладок проводит оплату, корзина очищается и данные о покупке копируются уже куда нужно. Так вот, если он на другой вкладке опять нажмет оплатить, то ему вернется ошибка, что его корзина пуста.

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

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

    1. Макс

      Ну, я вот не понимаю, Вадим, если Вы решили проблему нескольких вкладок путем «нелегкого» программирования, то честь Вам и хвала! Только вот есть куча примеров ситуаций, в которых просто НЕ НАДО пользователю дублировать вкладки, так при чем здесь ограничения, накладываемые на пользователя? Зачем его морочить корзинами какими-то, себя морочить алгоритмами? Раз уж пишете, что программирование — нелегкий процесс, так усложнять его нет смысла (ИМХО) :).

  5. Станислав

    Что-то я не понял этот код. Вы уверены, что этот код рабочий? И вообще, что если пользователь откроет одну вкладку. Вкладка получит свой ID, он будет записан в сессию на стороне сервера. Потом пользователь закроет эту вкладку и откроет страницу заново. Поскольку код сессии уже хранится на сервере, то мы получаем InvalidAccess, т.е. доступ к сайту вообще блокируется. Я решил задачу через localStorage (через jStorage.js для поддержки старых браузеров). При открытии любой страницы всегда устанавливается прослушка изменений глобальной переменной и после этого отправляется эхо-запрос (в виде установки переменной вида echo=random()). Если приходит эхо-ответ (из другой вкладки из такой же функции прослушки в виде установки переменной block=random()), то значит уже открыта другая вкладка и мы блокируем доступ к сайту. Только есть нюанс, что некоторые браузеры типа IE вызывают функцию прослушки даже для переменной установленных в этой же самой вкладке, а другие браузеры нет, поэтому нужно сравнивать random() отправляемый и значение получаемого, если они совпали, то это запрос с этой же самой вкладки, если не совпали, значит ответ пришёл из другой вкладки. Работает код нормально и под все живые браузеры. Минус в том, что это не синхронный обмен запрос-ответами, а ответ может прийти через некоторое время или не прийти вообще (если вкладка единственная), поэтому страница отрисовывается, скрипты начинают работать и потом уже (например, через 1 секунду) получаем эхо-ответ и блокируем страницу. Получается мерцание. В новых браузерах везде кроме Firefox есть задержка, в Firefox и в новых и в старых версиях обработка происходит мгновенно. В целом реализация рабочая, но не идеальная. Идеального решения похоже здесь нет.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *