Переносимый локальный secret (и «взлом» Макса)
Клиентский секрет сам по себе лежит на клиенте, а значит секретом уже не является. Со всеми вытекающими граблями и практиками. Сразу посмотрим, как сделать правильно.
Вот есть веб-приложение, которое после одного раза аутентификации передает на клиент некое секретное знание — будь то JWT, или просто уникальный токен — и дальше если этот секрет передали обратно (в приложение), приложение ему верит.
Схема распространенная как воздух. Неудивительно, что создатели популярного мессенджера сделали всё на отъебись выбрали именно её. На их месте, в общем-то, мог быть кто угодно другой: сделать нормально и правильно — это немного подумать надо.
Беда здесь в том, что мы доверяем единственному фактору. Даже если в полном flow аутентификации их использовалось несколько вплоть до анализа кала.
Когда аутентификация пройдена один раз, вот этот самый секрет становится единственным фактором, который всё определяет.
Занудно напомню терминологию:
— аутентификация — понять, кто к нам ломится
— авторизация — понять, чего ему сейчас можно делать
Схема с одним токеном, и одним секретом, говоря формально, имеет право на жизнь. Это как один ключ от двери в квартиру, и по массе никого особо не парит, что можно ключ передать соседу. Пока он не окажется в чужих руках, и следом окажется, что этого достаточно.
Собственно exploit в данном случае так и сработает, аналогично ключам: вытащи token из сессии браузера, руками, передай в другой браузер, и всё «заработает», тебе/злоумышленнику «дальше можно».
Вся защита на этом уровне строится вокруг того, чтобы ключик-токен нельзя было запросто вытащить.
Браузерные localStorage и аналогичные инструменты не рекомендуются для использования в такой схеме. Ладно что я могу открыть консоль и скопировать оттуда что-то руками. Хуже то, что любой исполняемый код в том же контексте, имеет те же самые возможности.
На веб-приложении доезжает ведро чужого кода из разных источников. Например — аналитика, реклама, трекеры ошибок итп.
Вы правда на 100% уверены, что никто их не сломает и не впихнет туда localStorage.getItem() с последующей пересылкой куда надо — ?
И как вы собираетесь это проверять?
Какие-то части данных можно пометить как «неизвлекаемые» (см про non-extractable), но из какого-то контекста прочитать их все-таки можно. И руками можно.
В этот момент уместно вспомнить про cookie, которые тоже локальное хранилище какой-то фигни. И вот как раз его можно пометить, как недоступное для клиентского кода — атрибуты httpOnly, политики sameSite. Вредоносный код не имеет к нему доступа, и иметь не будет; эти значения внутри себя обрабатывает только браузер, сам шлет в приложение, и руками их на клиенте трогать нельзя вообще, их даже не видно.
Сразу отсекает все атаки «уровня клиента приложения», доступ ограничен.
Это не панацея ни в коем случае, всё что сохранено в cookie jar и летит по сети — точно также можно спереть, только придумать как. Варианты есть.
Самый очевидный с XSS и внедрением кода мы (вроде как) отсекли.
Менее очевидные — остаются.
Для примера и последующего разбора представим, что я копирую руками полностью профиль браузера: со всеми хранилищами, cookie jar и иными «недоступными» фишечками. Если я перенесу такой профиль на другую машину, или сопру его при помощи malware на системе — вуаля. Это полностью валидный профиль. Как один, и единственный фактор для проверки со стороны приложения.
Вот так не надо делать, если мы хотим предусмотреть более одной проверки.
Я уже писал о том, что в 2026 расслабляться поздно, защита должна быть комплексной (раз) и постоянной (два). Комплексная говорит про то, что одному фактору доверять не стоит. Постоянной — о том, что «проверил и дальше всё можно» это слишком жирная и проблемная схема, в наших реалиях.
Ситуация, когда человеку малварь из ботнета сперла всё, что было на локальной машине — в 2026 реальность и данность.
Вместе с доступами почты, полным профилём, и даже, если малварь умная, с сетевыми параметрами: вредонос проксирует запросы прямо с машины пользователя. В ту минуту, когда ему этого захочется.
Вчера запросы были легитимными (с точки зрения авторства), прямо сейчас уже не очень. А ничего кроме времени технически не изменилось вовсе.
Как надо: во-первых, фингерпринтинг. Вместе с набором данных в профиле пользователя, у нас со стороны приложения есть возможность получить ряд других данных, которые связаны с другими параметрами. Например, адреса подключения. Например, технический фингерпринт фронтенда (даже браузера). Например, сетевой роутинг. Например, локальное системное хранилище секретов (которое, напомню, лежит не в браузере, а слоем ниже — это там где пины и хеши отпечатков пальцев).
Доверять только фингерпринтингу для аутентификации мы, понятное дело, не можем. Это не уникальная инфа. Но вот использовать его для детекции аномалий и взять оттуда факт совпадения технически в качестве второго фактора проверки — это как раз можем. Нам не надо «доверять»; нам надо поймать ситуацию, когда надо «не доверять». Это другая задача, она красиво решается как раз.
На практике, если мы (в приложении) знаем про конкретно этот токен, что для него изначально фиксировались вот такие гео, сетевой адрес, ключ браузера — а потом они почему-то не совпадают — не, парень, что-то тут не так. Давай-ка мы перестанем тебе доверять, и прогоним дополнительные проверки.
Во-вторых, набор сессий и история данных. Фингерпринтинг (сам по себе) не очень хорош тем, что он может меняться, и вообще не полностью уникален. Человек включил/отключил VPN, перескочил с мобильной сети на вайфай, поменял AS провайдера, обновил браузер — фингерпринты не совпали, и мы пользователю начнем портить жизнь проверками безопасности. Где-то будет оправдано, а где-то совсем не очень.
Поэтому имеет смысл вести отдельный, не привязанный к токенам, список успешных дополнительных пройденных проверок.
Отдельно список гео, с радиусом. Отдельно список IP и подсетей. Отдельно список автономных систем. Отдельно список браузеров и их фингеров.
Даже если он будет дофига большой — он все равно конечный. Ну да, у пользователя может быть 100 различных подсетей, штук 10 гео (сколько у него там VPN-ов), десяток локаций — ну камон, дом-работа-тёща-дача-любимое кафе, и все VPN сверху. Но их все равно ограниченное количество.
Если сетевые параметры изменились в сессии, но все равно изменение попадает в тот список, который мы уже ранее видели — это нормально, работай дальше. Если человек переключил VPN, но он это уже делал, и мы знаем его 2 адреса/подсети — да бог бы с тобой. Два, три, десять, сотня значений — это все равно лучше, чем «весь интернет».
Вот если мы конкретно это подключение не видели — иди перепроверяй. Но один раз: дальше мы эту проверку запомним, и больше компостировать мозг не будем. Fair enough.
В-третьих, это самое вкусное и достаточно сложное: гранулярный уровень доверия. Проверка аутентификации в бинарном смысле «всё можно/иди нафиг» — непозволительная местами роскошь, гибче надо стать. Внутри системы есть разные места, у них разные риски, разный уровень доверия.
Хочешь пользоваться приложением, но сменил подсеть, и мы ее знаем — да бог бы с тобой, валяй. А для того, чтобы пойти копаться в админке и биллинге, руками трогать настройки и деньги — неа, родной, давай-ка ты полностью пройдешь все проверки еще разок, и мы будем знать что они прямо сейчас честные (раз) и актуальные на текущую минуту (два).
Даже если всё совпадает, время проверок важно: вдруг у тебя ноутбук спёрли прямо в том кафе, с открытым браузером.
Таким образом, мы знаем и трекаем не только сессии (по ключам и токенам), но и проставляем каждой из них степень доверия. Проверялось час назад? Отлично. Совпали все параметры проверок? Умничка.
Поменялась подсеть или фингер браузера? Понижаем доверие до среднего по больнице, смотри свои гифки с котиками, но в биллинг тебе уже нельзя.
Сессия живет уже полгода, и вдруг очнулась спустя месяц неактивности? Это уже минималочка, на тоненького, тебе доступно только чтение.
Ну и далее по аналогии, что у вас там в приложении есть.
Если говорить терминологически, аутентификацию мы не трогаем, а вот авторизация («что можно») в этом кейсе обрастает дополнительно фичами и возможностями.
Если расширять по аналогии пример с «ключами от квартиры», то давайте предположим, что у нас отрастает консьерж, и дополнительные проверки по широкому ряду признаков.
- Вот есть список людей, у которых в принципе может быть ключ. Кто-то пришел незнакомый — вопросы.
- Вот у нас есть sanity check, когда всяких товарищей в абибасах гоповатого вида дополнительно проверят на входе; тут мутных личностей и человеках в масках на пол-лица почему-то заранее не любят.
- Вот у нас есть система видеонаблюдения с записью, чтобы найти концы, если что-то пошло не так.
- Вот пришли вроде бы знакомые люди, но выносят мебель и ценности мешками — это подозрительная и нечастая операция, давай-ка разбираться.
Отдельно галочка про проверяемость: неплохо бы иметь возможность, какие параметры и комбинации сейчас считаются доверенными, для сессий работы.
А следом иметь возможность что-то оттуда выкинуть. Если я знаю, что доступа из Зимбабве в моем контексте точно быть не должно. Как минимум кнопка «разлогинить и выкинуть всё запомненное нафиг», для проблемного случая.
И логирование. Чего было, с какими параметрами, в какой сессии, с какими подключениями. Больше логов — чище жопа.
Вот это, все три штуки — правильный, современный и взрослый подход.