Предположим, перед разработчиком поставлен такой вопрос — как со стороны веб-сайта определить, что у пользователя установлено конкретное приложение? Вопрос интересный. На него есть несколько способов ответа. Как вам такой вариант — поставить в систему уникальный шрифт при инсталляции программы? Ведь браузер всегда отдаёт по запросу список системных шрифтов. Значит, проблема решена.
Так делают различные программы, хотя это не назовёшь образцом правильного программирования. У метода свои преимущества и недостатки.
Например, в старых версиях TeamViewer 8 метод работал следующим образом.
Непосредственно скрипт для определения присутствия шрифта располагался здесь (сейчас удалён):
зеркало
Скрипт для подключения:
зеркало
Роберт Хейст, директор по информационной безопасности TeamViewer, прокомментировал ситуацию следующим образом:
Установка на компьютер пользователя проприетарного шрифта — один из способов пометки устройства. Разработчики не видели в этом ничего аморального или незаконного (собственно, ничего противозаконного здесь нет). Пользователю не сообщается о таком методе пометки, потому что ему не положено разбираться в тонкостях работы приложения. Директор по информационной безопасности сказал, что это удобно «для всех групп пользователей» (в том числе для «простых людей»).
Однако через несколько дней после разоблачения и публичного обсуждения нативного шрифта компания TeamViewer решила от него отказаться. Но это не значит, что такой способ распознавания пользователей не используют другие программы. Технически это просто один из вариантов фингерпринтинга. В каком-то смысле, это проприетарная разновидность куков.
Вообще, способов распознавания «заражённой» системы есть огромное количество. Но эти методы «свой-чужой» используют преимущественно создатели вредоносных программ (например, для контроля и управления ботнетом).
Если вы придумали свой оригинальный способ пометки компьютера — то получаете ряд преимуществ, например:
В принципе, это один из примеров применения принципа «безопасность через неясность» (security through obscurity). Преимущества и недостатки этого принципа хорошо известны. Обычно он считается опасным и вредным, но в некоторых отдельных случаях всё-таки есть рациональные основания прибегать к секретности. Хотя отрицательные стороны тоже присутствуют. Главная проблема — что любой посторонний сайт может определить, какое программное обеспечение установлено на вашем компьютере, что не очень хорошо с точки зрения безопасности (именно поэтому TeamViewer принял решение отказаться от этого метода).
Стандартный способ запускать нативное приложение из браузера — это Web App Manifest. Например, iOS запускает нативные программы через адрес
Вероятно, при использовании стандартного способа запуска приложений у сообщества информационной безопасности не возникнет претензий к разработчику.

Так делают различные программы, хотя это не назовёшь образцом правильного программирования. У метода свои преимущества и недостатки.
Например, в старых версиях TeamViewer 8 метод работал следующим образом.
- Чтобы открыть сессию с другим пользователем, программа генерирует пригласительный URL следующего вида:
https://get.teamviewer.com/v15/en/sXXXXXXXX
… гдеXXXXXXXX— это код сессии.
- Сайт проверяет с помощью JavaScript, что у пользователя присутствует шрифт TeamViewer. Это означает, что программное обеспечение TeamViewer установлено в системе.
Поскольку инсталлятор зарегистрировал в операционной системе обработчик протоколаteamviewer8://, то веб-сайт (код JavaScript) может напрямую вызвать TeamViewer по следующему URL:
teamviewer8://instantsupport/?sid=XXXXXXXX
- Если шрифт в системе не обнаружен, то сайт предлагает пользователю скачать и установить приложение.
Непосредственно скрипт для определения присутствия шрифта располагался здесь (сейчас удалён):
https://get.teamviewer.com/get/res/scripts/fontdetect.js
зеркало
Код в деобфусцированном виде
if (!self.__WB_pmw) { self.__WB_pmw = function (obj) { this.__WB_source = obj; return this; }; } { let window = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("window") || self.window; let self = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("self") || self.self; let document = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("document") || self.document; let location = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("location") || self.location; let top = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("top") || self.top; let parent = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("parent") || self.parent; let frames = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("frames") || self.frames; let opener = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("opener") || self.opener; function detect(a) { var c = document.getElementsByTagName("body")[0], b = document.createElement("span"); b.style.fontSize = "72px"; b.innerHTML = a; b.style.fontFamily = "nonexistingfonttoforcedefault"; c.appendChild(b); var e = b.offsetWidth, d = b.offsetHeight; c.removeChild(b); b.style.fontFamily = a + ",nonexistingfonttoforcedefault"; c.appendChild(b); a = b.offsetWidth != e || b.offsetHeight != d; c.removeChild(b); return a; } function minimalisticMajorCheck(a) { a = "TeamViewer" + String(a); return detect(a) ? true : detect(a + "Host") ? true : false; } function getMajorVersionArray() { for (var a = 0, c = [], b = 99; 1 <= b; b--) minimalisticMajorCheck(b) && (c[a++] = b); return c; } function getCharWidth(a, c, b) { var e = "TeamViewer" + String(a); a = document.getElementsByTagName("body")[0]; var d = document.createElement("span"); d.style.fontSize = "10px"; d.style.fontFamily = e; d.innerHTML = String(c); a.appendChild(d); c = d.offsetWidth; a.removeChild(d); b && 10 == c && (c = 0); 10 < c && (c = -1); return c; } function getVersionFromString(a, c) { for (var b = 0, e = false, d = 0; d < c.length; d++) { var f = getCharWidth(a, c[d], true); if (0 > f || 9 < f) e = true; b = 10 * b + f; } return e ? -1 : b; } function CheckForTeamViewer() { var a = getMajorVersionArray(); return 0 < a.length ? a[0] : 0; } ; }
Скрипт для подключения:
https://get.teamviewer.com/get/res/scripts/connect.js
зеркало
Код в деобфусцированном виде
if (!self.__WB_pmw) { self.__WB_pmw = function (obj) { this.__WB_source = obj; return this; }; } { let window = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("window") || self.window; let self = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("self") || self.self; let document = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("document") || self.document; let location = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("location") || self.location; let top = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("top") || self.top; let parent = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("parent") || self.parent; let frames = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("frames") || self.frames; let opener = self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init("opener") || self.opener; function isBlackListed(e) { if (blackList = new Array(42, 43, 44, 47, 59, 61, 91, 92, 93, 106, 107, 110, 111), 65 <= e && e <= 82) return true; if (84 <= e && e <= 90) return true; if (145 <= e && e <= 188) return true; if (190 <= e) return true; for (i = 0; i < blackList.length; i++) if (blackList[i] == e) return true; return false; } function checkIdFormat(e, t) { var n; if ((t = t || window.event).which ? n = t.which : t.keyCode && (n = t.keyCode), t.ctrlKey) return true; if (t.altKey) return false; if (isBlackListed(n)) return false; var i = ""; return e && e.value && (i = e.value), theMidLength = i.length, (83 != n || 0 == theMidLength) && (48 <= n && n <= 57 || 96 <= n && n <= 105 ? (t.shiftKey || (0 == theMidLength && (e.value = "s"), 3 != theMidLength && 7 != theMidLength || (e.value = e.value + "-"), 96 <= n && n <= 105 && (n -= 48), e.value = e.value + String.fromCharCode(n)), false) : 109 != n && 189 != n && 45 != n && 32 != n || (3 != theMidLength && 7 != theMidLength || (e.value = e.value + "-"), false)); } var timestamp; function SetFallbackUrl(e) { timestamp = new Date, setTimeout(function () { new Date - timestamp < 1e3 && window.location.replace(e); }, 500); } function SetFallbackUrlNoCheck(e) { timestamp = new Date, setTimeout(function () { window.location.replace(e); }, 10); } function OpenLinkWithFallbackNoFrame(e, t) { SetFallbackUrl(t), window.location.replace(e); } function OpenLinkWithFallback(e, t) { SetFallbackUrl(t); t = document.createElement("iframe"); t.style.display = "none", document.body.appendChild(t), t.src = e; } function OpenLinkWithFallbackWithoutIframe(e, t) { SetFallbackUrlNoCheck(t), window.location.replace(e); } function OpenLinkWithoutFallback(e) { window.location.replace(e); } function TryConnectWithTV(e, t) { var n, i; 8 < CheckForTeamViewer() ? (n = e, i = document.getElementById("dDownloadTeamViewer"), e = document.getElementById("dStartTeamViewer"), i && e && (i.style.display = "none", e.style.display = "")) : n = t, window.location.replace(n); } function TryConnectWithTVWithPreferenceCheck(e, t, n, i) { var o, a, r; 8 < CheckForTeamViewer() ? (o = e, a = document.getElementById("dDownloadTeamViewer"), r = document.getElementById("dStartTeamViewer"), a && r && (a.style.display = "none", r.style.display = ""), window.location.replace(o)) : "" === (a = getCookie("getuserpreference")) ? ($("#userPreferenceDialog").attr("title", "Connect to " + n), r = $(window).width(), n = $(window).height(), $("#userPreferenceDialog").dialog({autoOpen: false, height: 0.32 * n, width: 0.3 * r, modal: true, show: {effect: "fade", duration: 1e3}}).dialog("open"), $("#tdFullClient").click(function () { HandlePreferenceSelection(e, "launch"); }), "true" === i ? $("#tdQuickSupport").remove() : $("#tdQuickSupport").click(function () { HandlePreferenceSelection(t, "download"); })) : (o = "launch" === a ? e : t, window.location.replace(o)); } function HandlePreferenceSelection(e, t) { window.location.replace(e), $("#chkRememberSelection").is(":checked") && setCookie("getuserpreference", t, 365); } function setCookie(e, t, n) { var i = new Date; i.setTime(i.getTime() + 24 * n * 60 * 60 * 1e3); i = "expires=" + i.toUTCString(); document.cookie = e + "=" + t + ";" + i + ";path=/"; } function getCookie(e) { for (var t = e + "=", n = decodeURIComponent(document.cookie).split(";"), i = 0; i < n.length; i++) { for (var o = n[i]; " " === o.charAt(0);) o = o.substring(1); if (0 === o.indexOf(t)) return o.substring(t.length, o.length); } return ""; } function GetTextLength(e, t, n, i) { var o = $("#divMeasureText"); return o.css("font-family", t).css("font-size", n).css("font-weight", i), o.text(e), o.width(); } }
Роберт Хейст, директор по информационной безопасности TeamViewer, прокомментировал ситуацию следующим образом:
Шрифт TeamViewer используется для реализации плавного перехода пользователя от веб-клиента к нативному клиенту, например, при подключении по пригласительной ссылке, для предложения установки или прямой инициации подключения. Это оказалось полезным для улучшения пользовательского опыта у всех групп пользователей. Тем не менее, учитывая возникшую обеспокоенность, мы решили пересмотреть и изменить этот подход в одной из следующих версий, чтобы предотвратить возможное обнаружение установки TeamViewer через шрифт.
Пометка устройства
Установка на компьютер пользователя проприетарного шрифта — один из способов пометки устройства. Разработчики не видели в этом ничего аморального или незаконного (собственно, ничего противозаконного здесь нет). Пользователю не сообщается о таком методе пометки, потому что ему не положено разбираться в тонкостях работы приложения. Директор по информационной безопасности сказал, что это удобно «для всех групп пользователей» (в том числе для «простых людей»).
Однако через несколько дней после разоблачения и публичного обсуждения нативного шрифта компания TeamViewer решила от него отказаться. Но это не значит, что такой способ распознавания пользователей не используют другие программы. Технически это просто один из вариантов фингерпринтинга. В каком-то смысле, это проприетарная разновидность куков.
Вообще, способов распознавания «заражённой» системы есть огромное количество. Но эти методы «свой-чужой» используют преимущественно создатели вредоносных программ (например, для контроля и управления ботнетом).
Преимущества проприетарных куков
Если вы придумали свой оригинальный способ пометки компьютера — то получаете ряд преимуществ, например:
- метод работает даже в том случае, если у пользователя в системе не поддерживаются или заблокированы куки;
- системный шрифт не удаляется по таймеру, у него нет срока действия, как у куков;
- системный шрифт не удаляется при очистке куков из браузера;
- на запись куков требуется спросить разрешение по закону ЕС, на установку системного шрифта пока не требуется;
- и др.
В принципе, это один из примеров применения принципа «безопасность через неясность» (security through obscurity). Преимущества и недостатки этого принципа хорошо известны. Обычно он считается опасным и вредным, но в некоторых отдельных случаях всё-таки есть рациональные основания прибегать к секретности. Хотя отрицательные стороны тоже присутствуют. Главная проблема — что любой посторонний сайт может определить, какое программное обеспечение установлено на вашем компьютере, что не очень хорошо с точки зрения безопасности (именно поэтому TeamViewer принял решение отказаться от этого метода).
Стандартный способ — Web App Manifest
Стандартный способ запускать нативное приложение из браузера — это Web App Manifest. Например, iOS запускает нативные программы через адрес
/.well-known/apple-app-site-association на домене. Поддержка манифестов related_applications и prefer_related_applications сейчас находится на экспериментальной стадии в разных браузерах.Вероятно, при использовании стандартного способа запуска приложений у сообщества информационной безопасности не возникнет претензий к разработчику.

