Пользователь
0,0
рейтинг
19 августа 2011 в 11:54

Разработка → Постоянные неблокируемые cookie с использованием HTTP-заголовков перевод

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

Использование ETag (сокрашение от 'element tag', «метка элемента») для отслеживания пользователей известен и используется в партнерских сетях с начала прошлого десятилетия. Так же известно, что и заголовок Last-Modified(spec) теоретически может использоваться для отслеживания пользователей с помощью уникального значения времени обновления.

Мне, правда, кажется, что мало кто знает, что заголовок Last-Modified может принимать в качестве значения любую строку, то есть значение не обязательно должно быть правильной датой.

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

Первый запрос

Превый запрос к серверу выглядит так. Ничего необычного не происходит.

GET /tracking-cookie HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cache-Control: max-age=0
Host: nikcub.appspot.com
User-Agent: Mozilla/5.0


Ответ сервера: устанавливам токен

Сервер отвечает и устанавливает уникальный идентификатор (в данном случае UUID) в качестве значения для Last-Modified:

HTTP/1.0 200 OK
Server: Dev/1.0
Date: Sat, 19 August 2011 7:48:25 GMT
Content-type: text/html; charset=utf8
    Last-Modified: d5ee23de-ca05-11e0-ab0b-c336b05508a0
Cache-Control: no-cache
Content-Length: 1634


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

Last-Modified: Sat, 29 Oct 1994 19:43:31 GMT

Последующие вызовы

Браузер теперь будет отсылать этот токен при каждом вызове к тому же URI, используя заголовок If-Modified-Since(спека). Браузер спрашивает: «Если дата изменения данного ресурса позже, чем эта дата, пришли его мне», но при этом отсылает уникальный идентификатор, а не дату.

GET /tracking-cookie HTTP/1.1
Host: nikcub.appspot.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0
    If-Modified-Since: d5ee23de-ca05-11e0-ab0b-c336b05508a0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3


Это срабатывает даже, если закрыть браузер и открыть его заново, и срабатывает во всех основных браузерах. Метод, основанный на ETag'ах работает не всегда, особенно если на пути стоят web-прокси, а вот метод, основанный на Last-Modified срабатывает всегда.

Решения

Проблема с этими методами заключается в том, что они обходят пользовательские и программные настройки безопасности, связанные с cookie. Можно заблокировать любые cookie, но ETag, Last-Modifed и другие методы все равно позволят отслеживать ваш браузер.

В спецификации Last-Modified указано, что значение должно быть датой, но с примечанием, что в случае несинхронизированных часов могут возникнуть возможные проблемы. Большинство библиотечных реализаций просто отсылают это значение обратно без проверки, в частности потому что парсинг дат — это сплошная головная боль. Браузеры поступают так же, что приводит к наличию описываемой проблемы. Это значит, что Last-Modified работает как cookie, но без каких-либо проверок по безопасности.

Я отошлю bug report во все браузеры с открытым исходным кодом с просьбой корректно парсить даты. Это не стопроцентное решение, так как можно продолжать отследивать пользователей, используя уникальные даты. Но, возможно, будет найден решение в виде приведения даты к ближайшему часу или других базовых проверок на валидность даты. Другого решения, кроме как очистить и отключить кэш, нет, но условные GET-запросы все равно происходят во время браузерной сессии в некоторых браузерах.

Можете сами убедиться в этой проблеме на моей странице.

Дополнение: Плагин для конфиденциальных данных, над которым я работаю, Parley, решит эту проблему, так как он блокирует запросы с любых сторонних сайтов. Я думаю о том, чтобы добавить к нему и парсинг дат. Вот над чем придется поработать, когда я снова возьмусь за этот проект (плагины могут немногое, и иногда возникает соблазн создать форк WebKit'а и сделать собственный безопасный браузер).

Upd. Компания KissMetrics не подавала в суд на авторов исследования, а подала встречный иск против юридической компании и клиентов этой компании, которые подали на KissMetrics в суд. Автор исследования, Ashkan Soltani, к этим искам отношения не имеет.
Перевод: Nik Cubrilovic
Dmitrii 'Mamut' Dimandt @dmitriid
карма
0,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (71)

  • +1
    Разные веб-сервера шлют дату в разных форматах, иногда не в стандартных. Соответственно браузерам нужно под их формати подстраиватся, а это не только геморно но и не всегда возможно (т.к. для одной строки могут подходить больше одного стандарта). Думаю проблемку нужно на уровне спецификация протокола решать…
    • +4
      Спека, правда, уже говорит об этом:
      The Last-Modified entity-header field indicates the date and time at which the origin server believes the variant was last modified.
             Last-Modified  = "Last-Modified" ":" HTTP-date
      



      А HTTP-date обозначен тут: www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1

      То есть, в теории, все уже заспецифицировано и запротоколировано по самое не хочу. Но как всегда, все на спецификации глубоко наплевать :)
      • 0
        HTTP-date допускает три разных формата, плюс время указывается с точностью до секунды, итого в одних сутках содержится 259 200 различных уникальных идентификаторов. Учитывая, что на реальное значение Last-modified нам плевать, и мы можем хоть прошлый год там выставить — ситуация не намного лучше, чем с произвольной строкой.
        • 0
          Это тоже верно. Как автор и указал, это — только частичное решение, а полного, увы, нет
          • +2
            Ну, можно в некоторых разумных пределах fuzzing поля делать.
            15 секунд туда-сюда при каждом запросе. Чем более старая дата — тем больше допустимая амплитуда отклонения.

            Процент ложных запросов мимо кеша вырасти особо не должен в подавляющем большинстве случаев, а у идентификации возникнут очевидные проблемы.
            • 0
              Проблему это не решит. Кто мешает выделять на одного пользователя для отслеживание хоть амплитуду в одну — две недели?
        • 0
          Браузер должен парсить дату чтобы определить формат используемый сервером, а потом в If-modified-since выставлять её по системным часам клиента, а не тому что отдал сервер. :]
          • 0
            И чем это усложняет идентификацию?
            • 0
              Действительно, чем усложнит идентификацию тот факт, что мы выдаём клиенту один идентификатор, а он возвращается с другим?
              • +1
                Если мы указываем часовой пояс, то ничего не изменилось — конвертнули в нужное время, получили тот же токен.

                Если мы не указываем часовой пояс, то рушим работу с нормальными серверами.
                • +2
                  Я так понял, нужно в If-Modified-Since подставлять значение часов клиента на момент получения предыдущей страницы. В этом случае совершенно всё равно, что там прислал сервер в прошлый раз.
                  • 0
                    Тогда у нас сразу же возникают проблемы с кэшированием и показыванием правильной версии клиенту.
              • +1
                У меня два предположения, что вы подразумеваете под фразой «выставлять её по системным часам клиента»: либо менять часовой пояс, либо посылать текущее время. Второй случай не несет в себе смысла.
    • 0
      Цитата> Метод, основанный на ETag'ах работает не всегда, особенно если на пути стоят web-проки, а вот метод, основанный на Last-Modified срабатывает всегда. <Цитата

      Нет, тоже не всегда. Только что проверил — мой прокси в запросе If-Modified-Since указывает тот момент времени, когда он поместил этот объект в кэш, а не ту строку, которую сервер выдавал в тот момент.
  • 0
    Занятно, спасибо за перевод.
    • +1
      Переводил в спешке, поэтому могут быть неточности и кривости в изложении :) Главное, ногами не бейте :)
      • +2
        Тут главное суть проблемы, а она отлично понятна.
  • 0
    В Опере пример не работает, Last-Modified не ставится.
    Как-то сомнительно использование этого трюка для идентификации, ведь браузер пошлет If-Modified-Since только на тот же самый url, на другие он слать не будет.
    • +2
      В Эксплорере тоже. «Срабатывает во всех основных браузерах» вызывает сомнение.
    • +2
      Трюк нужен для сайтов, ставящих отслеживающие куки. То есть достаточно поставить img href=«tracker-site/url/», чтобы получать информацию о том, куда и как ходит пользователь
      • 0
        А как вы получите информацию, куда он ходит? По рефереру?
        • 0
          Можно по рефереру. Можно по дополнительному токену в url'е.
          • 0
            В том то и дело — изменяем url, браузер больше не шлет If-Modified-Since.
            • –1
              И кто же будет менять этот url?
              • 0
                Вы, раз собираетесь в url добавлять токен с информацией о текущей странице.
                • –1
                  Блин. Я захожу на страницу site.url, на нем есть <img url="site2.url">. site2.url и есть сайт, который отслеживает ваши перемещения. Что и как вы собираетесь менять?
                  • 0
                    Как он отслеживает перемещения? Каким образом, кроме реферера, он может узнать, на какой странице стоит тег <img url="site2.url">? Вы упомянули про тукен в url, где он здесь?
                    • 0
                      Что ему мешает узанть из referer'а?
                      Что мешает автору сайта site1.url добавить токен в урл?

                      Какой это отношение имеет к вопросу «И кто же будет менять этот url?»?
                      • 0
                        > Что ему мешает узанть из referer'а?
                        Ничего не мешает узнать по рефереру, просто вы упомянули второй способ, который я у вас выпытываю.

                        > Что мешает автору сайта site1.url добавить токен в урл?
                        В какой url? В site2.url?
                        • –2
                          Да. В site2.url.

                          Начнем с начала.

                          Вы — пользователь. Вы открыли бразуер по адресу site.url, на нем есть <img url="site2.url">. site2.url и есть сайт, который отслеживает ваши перемещения

                          Без разницы, что находится в site2.url.

                          Вы задаете вопрос:
                          В том то и дело — изменяем url, браузер больше не шлет If-Modified-Since.


                          Кто будет менять этот самый site2.url?
                          • 0
                            Дальше, пожалуйста. «Xтобы получать информацию о том, куда и как ходит пользователь». Я захожу на site.url/info/, что там?
                            • –2
                              Блин. Такое ощущение, что я разговариваю со стенкой.

                              Заходите туда, там есть страница, на которой в самом низу есть картинка размером 1x1, которая берется с сайта site2.url. Хоть с токеном, хоть без токена — без разницы.

                              Дальше. Куда применять ваше
                              В том то и дело — изменяем url, браузер больше не шлет If-Modified-Since.

                              ?
                              • –3
                                Ага, в данном случае стенка — вы.

                                Если адрес картинки на сайте site2.url будет точно такой-же как на странице site.url/, то сайт site2.url получит возможность идентифицировать пользователя, но он никак не узнает, на какую страницу попал пользователь. Все что он сможет сказать — сколько раз каждый пользователь дернул url.

                                Если же мы передаем в url для site2.url какой-то тукен с зашифрованных url первого сайта, то site2 сможет сказать, какие урлы открывались на первом сайте. Но вот беда, он не сможет определить, один и тот-же это был пользователь, или нет, потому что урлы-то разные.
                                • 0
                                  > но он никак не узнает, на какую страницу попал пользователь

                                  referer'ы у нас уже отменили?

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

                                  это тож решаемо. Более того, полезно для сбора статистики
                                  • 0
                                    ах. и да. url/ и url/?token=xxxx — это один и тот же URL
                                    • 0
                                      • 0
                                        ?
                                        • 0
                                          OMG!
                                          dmitriid> Трюк нужен… чтобы получать информацию о том, куда… ходит пользователь

                                          homm> А как вы получите информацию, куда он ходит?

                                          dmitriid> по дополнительному токену в url'е

                                          homm> В том то и дело изменяем url, браузер больше не шлет If-Modified-Sinceа это, по мнению homm`а противоречит Вашему первому утверждению! После чего, как я понимаю, Вы полностью запутались выясняя что и как
                                          • –1
                                            Бл*ть.

                                            КТО будет изменять URL?

                                            Вы — пользователь. Вы открыли бразуер по адресу site.url, на нем есть . site2.url и есть сайт, который отслеживает ваши перемещения

                                            Без разницы, что находится в site2.url.

                                            Кто будет менять этот самый site2.url?
                                            • 0
                                              > site2.url и есть сайт, который
                                              > отслеживает ваши перемещения
                                              Как он их отслеживает без реферера? Как он для этого использует упомянутый вами токен?
                                              • –1
                                                http://site2/url?token="/any/token/"
                                                |________1______||_________2_______|
                                                


                                                1 = RFC 3986. Uniform Resource Identifier. Section 3.3. Path

                                                2 = токен
                                                • 0
                                                  3.3. Path — это не то, что вы выделили, а только «/url», до этого идут домен и схема.
                                                  • –1
                                                    Да, согласен.
                                    • 0
                                      Вы уже бред несете.
                                  • +3
                                    > referer'ы у нас уже отменили?
                                    Но вы же хотели рассказать способ без реферерров!!!111
                                  • 0
                                    > это тож решаемо.
                                    Но решаемо не с помощью уязвимости, о которой вы сделали перевод. Да?
                                • –2
                                  На site1.com/page1 стоит картинка 'site2.com/tracker.png?urlhash=' +md5('site1.com/page1'). Так мы будем знать и куда конкретно заходил, а не только факт захода.
                                  • 0
                                    Ну и каким макаром в запрос на адрес
                                    'site2.com/?'+md5('/page1') попадет заголовок If-Modified-Since из заголовка ответа Last-Modifed от запроса на 'site2.com/?'+md5('/page2'), если это разные урлы?
                                    • 0
                                      В первый раз сервер установит «этот клиент первый раз посетил эту страницу», а во второй уже можно будет отслеживать сколько раз он её посещал, а если будет уходить и на другие уже посещенные страницы, то и маршруты отслеживать. Не столь универсально как куки, токены и реферы, но лучше чем ничего, если клиент параноидальный и всё это отключил, а про кэш забыл.
                                      • +1
                                        а если будет уходить и на другие уже посещенные страницы, то и маршруты отслеживать
                                        Как? Нет — КАК??!!! У вас не будет данных, что человек, зашедший по md5('/page1') и md5('/page2') — один и тот же, или это разные люди. Браузер не пошлет заголовки одного адреса по другому.

                                        Я допускаю, что я чего-то очевидного в упор не замечаю, но что-то пока никто на это очевидное указать не может.
                                        • +1
                                          Да, тут я что-то ступил. Параметры в URL являются тоже частью «идентификатора» ресурса (в отличии от якоря). Отслеживать получится только как часто какой-то пользователь возвращается на конкретную страницу, но он ли посещает соседнюю страницу сказать нельзя будет без анализа других заголовков и служебной инфы типа айпишника.
                                    • –1
                                      это — не разные URL'ы. Читайте спеки, они рулят.
                                      • 0
                                        www.ietf.org/rfc/rfc1738.txt
                                        3.3. HTTP
                                        An HTTP URL takes the form:

                                        http://:/?
                                        • –1
                                          Парсер съел собки, поэтому приведу-ка я:
                                          An HTTP URL takes the form:
                                          
                                                http://<host>:<port>/<path>?<searchpart>
                                          


                                          После чего надо все же читать про URI тут: tools.ietf.org/html/rfc3986 особенно секцию 3.3 path:
                                          The path component contains data, usually organized in hierarchical
                                          form, that, along with data in the non-hierarchical query component
                                          (Section 3.4), serves to identify a resource within the scope of the
                                          URI's scheme and naming authority (if any). The path is terminated
                                          by the first question mark ("?") or number sign ("#") character, or
                                          by the end of the URI.


                                          • 0
                                            Ну path, и че? А дальше идут 3.4. Query и 3.5. Fragment — следующие части url.
                                            • –1
                                              Осталось узнать, is-modified-since отправляется по URI path или по всему URL'у
                                              • 0
                                                Не пой части, что шлется на сервер, т.е. по url без Fragment, мистер «блядь, я читаю доки, разговариваю как со стенкой».
                                                • –1
                                                  Ваш поток сознания мне непонятен
                                                • 0
                                                  * по той части
  • 0
    Если браузер очищает кеш после закрытия, то и заголовки эти отправлять нет смысла.
    • 0
      Ну да, это — единственное решение.
  • 0
    Заголовок «Cache-Control: no-cache» заставляет фаерфокс и хром не отсылать If-Modified-Since и If-None-Match. В других браузерах не проверял.
  • +2
    вот все почему-то думают, что это плохо, один я не понимаю, почему, и как разработчик веб-приложений мне очень надо возможность отслеживать посетителей. в том числе и для того, чтобы этим же посетителям сделать жизнь на сайте лучше и комфортнее.
    • –1
      Благими намерениями выстлана дорога в… индекс Яндекса и Гугла :)
    • +1
      капитан подсказывает, что решение, отслеживать себя или нет, должен принимать все-таки пользователь

      вам же в реале не понравилось бы, если бы за вами по пятам ходил дядька и записывал все, что вы делаете, пусть даже вам от этого была какая-то польза?
  • +1
    Известный гик Chris Siebenmann ответил на это в своём блоге:
    I feel that changing Last-Modified is basically impossible for the browser to do in a way that is both safe and useful.
  • +1
    Обновите статью. На автора исследования никто иск не подавал — Смотреть «Note» внизу статьи.
    • 0
      Спасибо, исправил.
  • 0
    интересная штука, но вот у меня на Apache 2.2 + PHP FastCGI — не работает,
    а на NGinx + PHP-FPM — на ура. =)

    а все из-за бага:
    bugs.php.net/bug.php?id=50007
  • 0
    Кстати… а сами сервера парсят даты? Или просто формируют новую строку для Last-Modified и сравнивают её на равенство с переданной клиентом?

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