История о том, как я перестал добавлять новые функции и начал строить систему
После моей первой статьи про Pulse я не ожидал, честно говоря, примерно ничего. Но к моему удивлению увидел реакцию: немного поддержки, немного критики, несколько советов по UX.

Я получил около 7 тысяч просмотров, десятки комментариев и, неожиданно для себя, полноценный аудит проекта. Особенно, просто золотой грааль - комментарий от пользователя domix32. Он не обсуждал идеи. Не спорил про дизайн. Не рассуждал о будущем мессенджеров или самого Пульса. Он просто взял и проверил его на прочность.
И нашёл проблемы.
На момент начала аудита в Pulse уже было:
— 172 зарегистрированных пользователя;
— более 1100 сообщений (и нет, не все мои, а всего лишь 20%);
— около 60 пользователей, реально отправлявших сообщения;
— Если кто-то вдруг еще не в курсе - то так же имеются группы, приглашения, восстановление доступа, вход по устройству и PWA.
То есть речь уже шла не о прототипе, который я друзьям показал, мы с ним поигрались и все. А о системе, которой, на мое удивление, начали пользоваться реальные люди. Похоже тот самый хабрэффект сработал.
Когда критика полезнее похвалы
В комментарии было несколько конкретных замечаний:
не работает связь с разработчиком;
странно работает валидация меток;
есть вопросы к юридическим страницам;
уведомления между сессиями синхронизируются не идеально;
безопасность вызывает вопросы.
Но одна фраза запомнилась больше остальных:
За прочую безопасность даже не пробовал тестировать.
На первый взгляд обычный комментарий. Мысленно я очень сильно зацепился за именно эту часть комментария. Потому что она с одной стороны - короткая, но чертовски емкая. И формулировка внутри меня звучала уже так:
«Я уже нашёл достаточно. Если копнуть глубже — найду ещё.»
Именно после этого я остановил работу над новыми фичами и принял решение вместо очередного релиза с красивыми возможностями и двигаться в сторону безопасности, потому что сейчас публикуясь уже на Хабре, да и вообще привлекая новых пользователей в Пульс - этот вопрос встает очень и очень остро. Я закинул снапшот проекта чату GPT, с просьбой провести полный аудит, опираясь на комментарий и дополнительно проверить возможные дыры, баги и прочие дефекты проекта. Сколько же хлама в нем оказалось, но увы это последствия ранней архитектуры и вайбкодинга, так как все писалось с нуля с бешенным рвением, пусть и старался вести политику "реализуем исключительно правильные архитектурные решения". Но... Меня это от рефакторинга увы не спасло. В целом я этого ожидал.
Неожиданный результат аудита
Самым неожиданным открытием для меня стало даже не количество найденных проблем. Самым неожиданным оказалось то, что большинство из них были связаны между собой. Каждое исправление вытаскивало на поверхность следующий слой технического долга.
Например:
Сначала мы исправили доступ к диалогам. После этого стало сильно бросаться в глаза, что авторизация разбросана по нескольким обработчикам и требует отдельного AuthService. Когда начали выносить AuthService, выяснилось, что публичные и приватные DTO смешаны между собой. После разделения DTO стало заметно, что часть публичных endpoint вообще не ограничена по частоте запросов. После внедрения Rate Limiter обнаружились проблемы жизненного цикла WebSocket-соединений. А после ревизии WebSocket стало понятно, что отдельного внимания требует транспортный уровень и работа с origin-политиками. В какой-то момент стало ясно: это уже не набор отдельных багов.
Я прекрасно понимал, что все это последствия ранних архитектурных решений, которые были абсолютно нормальными для небольшого проекта, но начали создавать риски по мере роста системы уже на текущем этапе.
Поэтому вместо хаотичных исправлений появился технический roadmap. Не список новых функций. А список инженерных задач, которые постепенно устраняют накопленные архитектурные компромиссы.
Что именно предстоит исправить
Visibility-aware WebSocket Lifecycle
Сейчас браузеры умеют замораживать фоновые вкладки, а мобильные устройства агрессивно экономят батарею. В результате возникают ситуации, когда соединение технически существует, но фактически уже не используется.
Задача — сделать жизненный цикл WebSocket зависимым от состояния вкладки и активности пользователя.
Auth Hardening
Текущая система авторизации уже работает, но содержит упрощения, допустимые для ранних версий продукта. Предстоит перевести генерацию кодов на криптографически стойкие источники случайности, сделать атомарный claim запросов входа и усилить защиту сценариев восстановления доступа.
(Часть задач из первоначального roadmap уже была закрыта во время подготовки статьи. В частности, релиз 0.9.49 принёс AuthService Foundation и DB-based Rate Limiter. Поэтому следующий этап посвящён уже не построению базовых механизмов, а дальнейшему усилению безопасности существующей системы.)
Media Safety
Pulse использует отдельный микросервис обработки медиа. На раннем этапе основное внимание уделялось функциональности.
Теперь пришло время заниматься безопасностью загрузок:
ограничением размеров файлов;
проверкой MIME-типов;
защитой от SVG-атак;
контролем потребления памяти при обработке изображений.
Message History & Realtime Stability
История сообщений и realtime-события сегодня работают, но по мере роста объёма данных начинают проявляться эффекты гонок, устаревших ответов и пограничных состояний.
Отдельная задача — сделать работу истории полностью детерминированной независимо от скорости сети и порядка доставки событий.
Transport Security
На текущем этапе соединения уже работают через HTTPS и WSS. Следующий шаг — ужесточение транспортной безопасности:
проверка origin;
отказ от передачи чувствительных данных через query-параметры;
CSP и дополнительные защитные заголовки;
дальнейшая изоляция внутренних сервисов.
Security UX
Самая недооценённая часть безопасности — прозрачность для пользователя.
Появятся:
события безопасности;
дополнительные инструменты контроля доступа к аккаунту.
Пользователь должен понимать, что происходит с его учётной записью, а не просто доверять системе на слово.
Проблема №1. Историю чужого диалога нельзя защищать фронтендом
Одна из первых вещей, которую мы пересмотрели — доступ к истории сообщений.
История загружалась по conversation_id.
Условно:
GET /history?conversation_id=...
При этом сама архитектура предполагала, что фронтенд покажет пользователю только те conversation_id, которые ему принадлежат. Это удобное предположение.
И очень опасное.Если безопасность держится на том, что клиент «не знает идентификатор», значит безопасности фактически нет.
Решение
В релизе 0.9.48 появился ConversationAccessService. Теперь любой запрос к данным диалога проходит через одну точку проверки:
CanAccessConversation(userID, conversationID)
Если пользователь не является участником диалога или группы — получает:
403 Forbidden
Причём эта проверка используется не только для истории сообщений.
Через неё теперь проходят:
история сообщений;
закрепления;
скрытие диалогов;
mute-состояния;
часть realtime-операций.
Фактически появился единый слой авторизации доступа к данным. Что с точки зрения безопасности мне кажется вполне правильным.
Проблема №2. Email не должен утекать через публичные API
До аудита использовалась одна модель пользователя практически для всего. Это было удобно, но привело к интересному эффекту.
Публичные методы вроде:
/users /users/search /users/by-alias
могли вернуть больше информации, чем реально требовалось клиенту, но гораздо информативнее и полезнее человеку, который решил изучить API продукта чуток внимательнее и глубже. В том числе как оказалось в выдачу попадал и чувствительный email.
Решение
Разделить публичные и приватные данные по разным моделям. Модель была разделена на:
PublicUserDTO PrivateUserDTO
Теперь публичный API возвращает только действительно публичную информацию:
{ "id": "...", "username": "...", "alias": "..." }
Email остаётся доступным только владельцу профиля. На первый взгляд изменение маленькое. На практике именно из таких мелочей обычно и состоят настоящие утечки данных, которые потом радостно продаются, сливаются теми самыми взломщиками.
Проблема №3. Rate Limiter, который сначала не работал
Следующим шагом стал релиз 0.9.49.
Лимиты на вход, лимиты там и сям. Кто их любит? Из пользователей никто. А вот их отсутствие очень любят ломатели софта, так как это дает им неограниченные возможности для создания неограниченного количества запросов там, где надо бы их ограничивать. Отсюда появился DB-based Rate Limiter. И тут произошло самое интересное. Мы были уверены, что всё работает. Пока не начали тестировать.
Что пошло не так
В качестве идентификатора клиента использовался адрес подключения, который идет на бек с фронта. То есть фактически не реальный адрес пользователя, а наш собственный nginx. В логах это выглядело примерно так:
127.0.0.1:53120 127.0.0.1:53121 127.0.0.1:53122
Проблема оказалась в том, что порт менялся на каждом запросе. Для лимитера это были разные пользователи. Количество попыток никогда не достигало лимита. То есть защита существовала только на бумаге.
Решение
Пришлось разобрать следующее. Нынешний Reverse proxy был настроен, но не до конца. Как итог перелопатили:
работу nginx;
X-Real-IP;X-Forwarded-For;алгоритмы fixed window.
В результате мы переписали лимитер, и теперь ограничения работают для:
входа через устройство;
восстановления доступа;
поиска пользователей.
Во время тестирования запросов подряд, выходящих за пределы установленного лимита, теперь корректно получаем честный стоп:
429 Too Many Requests
Именно тогда стало понятно, что защита действительно работает и этот гештальт можно закрыть.
Проблема №4. Логи тоже являются поверхностью атаки
Ещё один интересный момент обнаружился во время ревизии. В логах присутствовали данные, которые не должны были туда попадать:
токены - я даже ужаснулся, что допустил такое;
части WebSocket payload;
некоторые служебные данные.
Пока проект маленький, кажется, что это не страшно. Но периодически возникают ситуации, когда ранняя архитектура, должна догонять новую. И средства отладки тоже - собственно мы целиком просканировали проект и выпилили отовсюду консольный дебаг.
Чем больше пользователей и инфраструктуры появляется, тем опаснее становятся собственные логи, ведь есть любители заглянуть на вкладки в консоль и сеть :)
В результате была проведена отдельная чистка логирования. Теперь чувствительные данные в логах отсутствуют.
Что изменилось в подходе
Самое интересное произошло не в коде. Изменился подход к разработке. Раньше цикл выглядел так:
идея → фича → релиз
Теперь он выглядит иначе:
идея → безопасность → архитектура → фича → релиз
Это медленнее.
Но эффективнее. Причем закрывает дыры в продукте раньше, чем их находят пользователи. И на мой скромный взгляд именно так хобби-проект постепенно превращается в систему.
Цена ранних решений
Самое интересное, что ни одна из найденных проблем не была результатом плохого решения. Почти все они были результатом правильных решений, принятых слишком рано или слишком поздно. Когда в системе 2 пользователя, централизованный сервис доступа кажется избыточным. Когда в системе нет восстановления доступа, кажется, что отдельный AuthService не нужен. Когда проектом пользуешься только ты сам и несколько твоих знакомых, друзей, то отсутствие Rate Limiter не кажется проблемой.
Но каждая такая временная упрощённая конструкция со временем превращается в технический долг. Именно поэтому большая часть работы последних дней (да-да именно дней) была не про новые функции, а про возврат архитектурного контроля над системой.
Что дальше
После аудита появилась отдельная дорожная карта технических релизов.
Следующие этапы:
Visibility-aware WebSocket Lifecycle;
Media Safety;
Message History Stability;
Transport Security;
Security UX.
Часть задач вообще не видна пользователю и функционально никак не ощущается. Но именно они определяют, выдержит ли проект рост и атаки на него.
Итог
Я точно знаю, что после публикации первой статьи проект стал лучше. Не потому что его похвалили. А потому что его раскритиковали. Иногда один технический комментарий даёт больше пользы, чем сотня лайков.
Поэтому если у вас есть собственный проект — покажите его людям. Особенно тем, кто способен найти в нём проблемы. Они могут оказаться лучшими помощниками в развитии продукта.
Спасибо всем, кто тестирует Pulse, пишет замечания и не боится критиковать.
И отдельное спасибо domix32. Некоторые архитектурные решения в проекте появились именно благодаря вашему комментарию.
Спасибо, что дочитали до этого места. Я открыт для любых замечаний, комментариев и пожеланий. А всем остальным желаю удачи, простых решений и легкого кодинга в своих проектах. Буду рад почитать ваши решения!
Комментарии (17)

gerbert_MX
21.06.2026 14:42ну это же.. ..база??? Безопасность на фронт-енде это вообще основа основ как не надо делать, тут даже бы нейросеть подсказала что так нельзя.
в тему бы было открыть исходный код - тут недавно была статья про месенджер на локальной сети и как по мне чем больше будет открытых проектов подобного толка тем быстрее мы получим оптимальный открытый и безопасный месенджер
почему за открытость - проблемы безопасности и тд это цветочки и та самая база, а вот ягодки это транспортный уровень Централизованый сервер сейчас нынче самое плохое для месенджера, особенно централизованный закрыто, то есть административно похоронить проект даже больше возможностей.

nikolas00_memew
21.06.2026 14:42У нас уже давно есть открытый, полноценный, безопасный, распределенный мессенджер - джабер. Дело за малым, осталось только написать для него хороший клиент на андроид и айфон, и поднять сервера которые будут бесплатно хранить мегатонны юзерских картинок, голосовух, и рилсов.
А, ну и еще надо будет как то людей в него завлечь, мессенджер без людей никому не нужен.

nikolas00_memew
21.06.2026 14:42Завлечь будет сложнее всего, ниша рабочего на парковке занята максом и его оттуда не подвинуть, телеграм с вотсапом занимают центральные места, и есть еще группа в полосатых купальниках по границам - тиктоки, инстакрапы итп. Какой у нас план?

qubs993 Автор
21.06.2026 14:42Как такового плана нет. Пульс это весьма нишевый продукт на мой взгляд, для тех кто устал от информационного шума. Хотя мне его просто интересно делать :)

gerbert_MX
21.06.2026 14:42да клиентов как грязи разных
есть даже полноценные фронт-енды аля социальная сеть по типу https://movim.eu/
с XMPP проблема в том что он огромный и массивный - поднять у себя selfhost целое приключение где чисто в параметрах настройки можно потонуть

qubs993 Автор
21.06.2026 14:42@gerbert_MX Спасибо за честность и конкретику. Вы безусловно правы: безопасность на клиенте — это одна из классических ошибок, которые я увы упустил на раннем этапе. В 0.9.48 мы перенесли проверку доступа к истории и диалогам на бэкенд через единый ConversationAccessService, а публичные DTO теперь не содержат email. В общем - уже реализовано :)
По поводу открытости: я не против открытого кода в будущем, но пока «Пульс» — это мой личный проект, и я хочу сначала довести архитектуру до такого состояния, чтобы открытие не превратилось в «посмотрите, как здесь всё страшно», а там и правда сейчас есть страшные решения, которые мне еще предстоит переделать... Стыдно - если кратко. А децентрализация — это интересный вызов, но для начала нужно пройти текущий этап: безопасность, стабильность, UX. Если проект выживет и окрепнет — можно будет подумать и об этом. Ещё раз спасибо за откровенность.

rsashka
21.06.2026 14:42Я получил … полноценный аудит проекта
Поздравляю, что открыли одно из свойств Хабра. Пользуюсь этим уже несколько лет

cless75
21.06.2026 14:42Если бы не было вначале кривой саморекламы , заходило бы лучше .
Сначала ценность потом реклама

qubs993 Автор
21.06.2026 14:42Учту в дальнейших статьях, я никогда не писал про свои пет-проекты на хабре, так только какие-то технические кейсы на старом акке были, поэтому прошу прощения. Спасибо, что подсветили!

micronull
21.06.2026 14:42Для поддержки пуш уведомлений посмотрите на https://ntfy.sh/ и https://unifiedpush.org пожалуйста.

domix32
21.06.2026 14:42«Я уже нашёл достаточно. Если копнуть глубже — найду ещё.»
врубить параноика в данной ситуации конечно хорошо, но это совершенно точно не то что я имел ввиду.
Проблема с поиском уязвимостей в том что это не слишком тривиально, когда делаешь условный 15-минутый забег на тестирование функционала. Можно наверное за это время успеть собрать несколько векторов атаки из разряд low hanging fruit - какие-нибудь инъекции в полях, XSRF, но у меня маловато опыта, чтобы такое провернуть с полпинка и не сказать чтобы много мотивации этим заниматься. Но то что это заставило задуматься о моделях угроз - определённо хороший шаг.
Теперь пришло время заниматься безопасностью загрузок:
про форк бомбы и rarjpeg ещё подумайте
Отдельно стоит подумать над тем чтобы пофаззить какие-нибудь публичные эндпоинты и посмотреть как оно будет себя чувствовать на каком-нибудь мусоре из юникода, например.
— 172 зарегистрированных пользователя;
вот тут вопрос - зарегестрированные пользователи - это те что с email или вообще любой? rate limit на регистрацию есть? кажется можно простым скриптом заспавнить тысячи пользователей без проблем. отдельный вопрос про disposable email а ля maildrop.cc, dropmail.me и прочие shitmail.
Возвращаясь к комментарию: это будет ultimate вариант аудита, когда ваш код будут ломать опираясь на код.

qubs993 Автор
21.06.2026 14:42Спасибо, что заглянули и сюда, и снова потратили время, которое в нынешнее время на вес золота.
Вы отчасти правы, мой «параноик» был скорее реакцией на сам факт: «кто-то взял и нашёл проблемы». То есть я понял для себя это так - не столько конкретные уязвимости, сколько общий подход, на который я подзабил спустя 2 недели после начала разработки) Хотя возможно я ошибаюсь...
По вашим новым пунктам:
Безопасность загрузок - уже в роадмапе (0.9.52). Fork bombs и gapreg (я правильно понимаю, что речь о расширениях/MIME-атаках?)
Фаззинг публичных эндпоинтов - вот это кстати очень интересная идея. Надо будет поковыряться и опасные точки определить/проверить/закрыть... Я даже не думал пока еще в эту сторону. Все хочу еще до различных автотестов добраться, но пока, увы, рук всего 2...
Rate limit на регистрацию - да, вот рейт лимитер в 0.9.49 уже вышел - на авторизацию, восстановление (к слову переделал попап в маленький баннер, а подтверждение входа вывел в "Устройства" в профиле), поиск. А вот на регистрацию кстати нет... Так что да - щас еще остается потенциальная дырка на ней, что не есть хорошо. Вы меня снова спасаете!)
Насчёт открытости кода - пока не готов, потому что стыжусь - надо еще подчистить хвосты, разделить модели. Проводить аудит такого кода - это кровь из глаз. Но эта идея остаётся в голове. Я думаю, что когда-нибудь я доберусь до вида - не стыдно показать публично.
Ещё раз большое спасибо!

domix32
21.06.2026 14:42gapreg
ze what? я вроде не опечатался. Речь именно за rarjpeg - приклеивание архивов(и не только) к картинкам. Меняешь расширение с jpg на rar/exe/msi - получаешь с виду новый бинарь, который не является картинкой. Поэтому в случае загрузки картинок имеет смысл обрезать всё что к картинке не относится. Как-то так обычно в соц.сетях имеются разные варианты загрузок - картинкой, документом, файлом. Mime атаки это несколько иной зверь.

qubs993 Автор
21.06.2026 14:42Про gapreq - промахнулся по клавишам, спешил.
Но это не меняет сути, таких проверок еще нет и это еще стоит реализовать.
schekinfs
без сарказма, по мессенджерам реальный дефицит. пили не останавливайся
qubs993 Автор
Спасибо! Именно это и держит. Когда видишь, что продукт кому-то нужен — руки сами тянутся к клавиатуре. Продолжу, обещаю.
uis246
XMPP, Matrix, тысячи их