Демо
Фавикон сайта — маленький значок .ico размером 16*16 или 32*32 пикселей на вкладке браузера. Помогает ориентироваться в сотнях вкладок. У твиттера синяя птичка, у Gmail красный символ почты, у Википедии жирное W.
Но оказывается, что эти значки представляют собой уязвимость, через которую можно выполнять фингерпринтинг — идентифицировать юзера даже через VPN и режим инкогнито в браузере (см. демо).
Немецкий программист Йонас Штреле (Jonas Strehle) в репозитории на Github описывает метод установки неудаляемого суперкуки через фавиконы: «Суперкуки через фавиконы присваивают посетителям сайта уникальные ID. В отличие от традиционных методов отслеживания, этот ID сохраняется практически навсегда и не может быть удалён пользователем с помощью простых методов. Метод отслеживания работает даже в режиме инкогнито. Суперкуки не удаляются при очистке кэша, закрытии браузера или перезапуске системы, использовании VPN или блокировщиков рекламы».
Как работает фингерпринтинг
Чтобы отобразить иконку, в код страницы вставляется такой атрибут:
<link rel="icon" href="/favicon.ico" type="image/x-icon">
Фавиконы должны быть очень легко доступны через браузер. Поэтому они кэшируются в отдельной локальной базе данных, так называемом кэше фавиконов (F-Cache), где хранится URL, идентификатор фавикона и время жизни.
Когда пользователь заходит на сайт, браузер проверяет локальный F-Cache на наличие записи, содержащей URL-адрес активного веб-сайта. Если запись найдена, значок загружается из кэша. Если запись отсутствует, браузер отправляет GET-запрос, чтобы загрузить фавикон с сервера.
Такой механизм позволяет серверу довольно много узнать о посетителе. Комбинируя статусы доставленных и не доставленных фавиконов для определённых URL, клиенту присваивается уникальный шаблон (идентификационный номер). Затем ID сохраняется:
const N = 4;
const ROUTES = ["/a", "/b", "/c", "/d"];
const ID = generateNewID(); // -> 1010 • (select unassigned decimal number, here ten: 10 -> 1010b in binary)
const vector = generateVectorFromID(ID); // -> ["/a", "/c"] • (because [a, b, c, d] where [1, 0, 1, 0] is 1 -> a, c)
После повторной загрузки сайта этот ID можно восстановить по списку сетевых запросов, отправленных клиентом для недостающих фавиконов — и таким образом идентифицировать браузер.
const visitedRoutes = [];
Webserver.onvisit = (route) => visitedRoutes.push(route); // -> ["/b", "/d"]
Webserver.ondone = () => { const ID = getIDFromVector(visitedRoutes) }; // -> 10 • (because "/a" and "/b" are missing -> 1010b)
Автор запустил сайт для демонстрации фингерпринтинга с помощью фавиконов. Опубликован исходный код и детальное описание механизма.
В этой уязвимости самое неприятное — это то, насколько легко можно обойти традиционные методы, которые люди используют для защиты своей приватности. По словам Штреле, фингерпринтинг пробивает «приватный» режим Chrome, Safari, Edge и Firefox. Очистка кэша, VPN или блокировщик рекламы — ничто не помешает вредоносным фавиконам.
К таким же выводам пришли исследователи из университета Иллинойса в опубликованной недавно научной работе "Tales of Favicons and Caches: Persistent Tracking in Modern Browsers": «Мы обнаружили, что сочетание нашей техники отслеживания на фавиконах с фингерпринтингом через неизменяемые атрибуты браузера позволяет сайту восстановить 32-битный идентификатор трекинга за две секунды», — говорится в исследовании. — В связи с серьёзностью уязвимости мы предлагаем внести изменения в кэширование фавиконов браузерами, чтобы предотвратить эту форму трекинга. Мы передали наши результаты разработчикам поставщикам браузеров, которые в настоящее время изучают варианты смягчения последствий».
На данный момент фингерпринтинг через фавиконы работает во всех крупнейших браузерах, в том числе в мобильных (значок плюса):
Браузер |
Windows |
MacOS |
Linux |
iOS |
Android |
Примечания |
---|---|---|---|---|---|---|
Chrome (v 87.0) | + | + | + | + | + | ? |
Safari (v 14.0) | ? | + | ? | + | ? | ? |
Edge (v 87.0) | + | + | ? | ? | + | ? |
Firefox (v 85.0) | + | + | ? | ? | ? | Другой фингерпринтинг в режиме инкогнито |
Brave (v 1.19.92) | + | + | + | ? | ? | ? |
В следующей таблице указано минимальное время, необходимое для проведения атаки. Реальный показатель зависит также от дополнительных факторов, таких как скорость интернет-соединения, местонахождение, производительность железа и тип браузера.
Редиректов (N бит) |
Кол-во различимых клиентов | Время записи | Время чтения | Масштаб атаки |
---|---|---|---|---|
2 | 4 | < 300 мс | < 300 мс | Один юзер с четырьмя браузерами |
3 | 8 | < 300 мс | ~ 300 мс | Примерная численность Кардашьянов |
4 | 16 | < 1 с | ~ 1 с | Кучка ваших соседей |
8 | 256 | < 1 с | ~ 1 с | Все ваши френды на Facebook |
10 | 1024 | < 1,2 с | ~ 1 с | Очень маленькая деревня |
20 | 1 048 576 | < 1,8 с | < 1,5 с | Небольшой город (Сан-Хосе) |
24 | 16 777 216 | < 2,4 с | < 2 с | Все Нидерланды |
32 | 4 294 967 296 | ~ 3 с | < 3 с | Все люди с доступом в интернет |
34 | 17 179 869 184 | ~ 4 с | ~ 4 с | Все люди с доступом в интернет и 4 браузерами у каждого |
Mingun
Что-то из примера непонятно — этот код выполняется на сервере или в браузере?
Если я правильно понял, то атака основана на том, что браузер часть favicon-ов закеширует, а часть нет при создании отпечатка. Но при повторном визите он закеширует все остальные — т.е. эта атака одноразовая?
easyman
Нет, ведь url favicon можно генерировать скриптом
Mingun
Ну и что? При первой же проверке вы на все юрл-ы сходите и закешируете их и придется еще один идентификатор генерировать. Так у вас быстро закончатся
1) идентификаторы
2) место в базе
alexxz
Судя по коду, там не вполне фингерпринтинг, а персистентная кука, биты которой считываются на основании того, за какими фавиконами браузер таки пошел на сервер. То есть для пользователя просто генерируется и записывается идентификатор.
Mingun
Ну так браузер пойдет за фавиконами только при первом посещении сайта, при этом он сразу же закеширует все полученные иконки, и никаких запросов при повторном посещении не будет. Поэтому мне непонятно, каким образом там собирались идентифицировать клиента. Даже если это удастся сделать в первый раз, то после первой же проверки все недостающие иконки будут дозагружены и больше никаких запросов не будет. И все браузеры станут неразличимыми.
rewiaca
Я всегда думал, когда глядел на окончание путей у CSS, js в виде ?random=27474838, что для браузера это каждый раз новый путь и новый файл. С favicon не проверял.
alexxz
С фавиконами — то же самое. Просто у сайта может быть, например, 32 фавикона (фавикон привязан к странице, а не домену). И факт их закешированности (читай, отсутствия запроса к серверу) — это признак конкретного браузера.
alexxz
Если сервер отвечает ошибкой, то браузер ошибку не кеширует, а пробует каждый раз заново. И именно этот признак используется в режиме "чтения".
maximw
Так сервер должен заранее знать ответить 200 или ошибкой на данный конкретный бит-фавиконку. Откуда сервер это знает?
Tatikoma
Сначала мы пробуем прочитать идентификатор, если его — тогда выполняем запись. Сервер может знать о том, какой сейчас этап.
alexxz
В режиме чтения сервер, грубо говоря, отвечает всегда ошибками, но при этом ещё косвенно передаёт на клиент за какими картинками по факту ходил браузер (фавикон загружен успешно из кеша). Если клиент видит, что браузер ходил за всеми картинками, то клиент понимает, что куки ещё нет и её надо проставить. В режиме записи сервер отдаёт 200 коды на 1 биты и 404 для 0 и браузер кеширует единички.
driver_by
В переводе опущен важный механизм собственно как это все работает :) Но есть ссылка на оригинал.
Там есть два режима — чтения (проверка идентификатора) и записи (запись идентификатора пользователя).
Идентификатор записывается редиректом по разным урлам. Это вот /a,/b,/c из статьи. По каким-то из них сервер на запрос фавикона отдает 200-ОК, по каким-то 404 (по сгенерированному заранее ID).
Соответственно чтение — прогнать пользователя по всем урлам и узнать какие фавиконы у него загружены, но при этом отправляя 404 в ответ. Т.е. при чтении браузер фавиконы и не получит (а сервер инфу получит). Если чтение показало, что фавиконов вообще нету, значит записи не было — прогоняем режим записи.
Как-то так.
korzhik
Видно что это перевод, но статья нигде не помечена как перевод, это уже норма на хабре?
driver_by
Скорее осуждаемое отклонение от нормы :)