Неделя 122. Три головы гидры

Разбор этой статьи
Эту тему разобрали в подкасте. Слушай параллельно с чтением.
У древнегреческой гидры была особенность: отрубаешь одну голову, вырастают две. На неделе 122 мы столкнулись с инженерной версией этого мифа. Починили уязвимость безопасности и обнаружили за ней вторую. Добавили кэширование, оно сломало интерфейс. Исправили цикл авторизации, он появился в другом месте. Каждое решение вскрывало следующую проблему. 229 коммитов за неделю, и ощущение, что бежишь по эскалатору в обратную сторону.
Я Дмитрий Дьяконов, основатель Botseller AI. Это сорок третий выпуск серии «Ретроспектива», бортжурнал в прошлое: от настоящего к первому коммиту. Сегодня отматываю на 1-7 декабря 2025 года. Продукту без малого пять месяцев. Через неделю начнётся великий пивот CRM, но сначала нам предстоит пережить четверг, который не закончился до трёх ночи, и кризис безопасности, который заставил переписать три слоя за 48 часов.
Контекст: на пороге большой стройки
Начало декабря, продукт растёт. Боты работают, клиенты подключаются, появляются заказчики с десятками и сотнями сделок. Один из них продаёт недвижимость на Пхукете и генерирует столько запросов, что мы упираемся в лимиты внешней CRM-системы. Другой запрашивает тестовый чат, чтобы проверять ответы бота прямо из панели управления. Третьему нужна воронка продаж с настраиваемыми этапами.

Мы строим всё это одновременно. И параллельно начинаем замечать, что фундамент, на котором стоит платформа, не так крепок, как казалось.
Голова первая: кризис безопасности
В четверг обнаружили, что система безопасности данных, которую мы считали надёжной, не работает на целом слое. Фильтрация по принадлежности заказчика просто не применялась в одном из сервисов.

Но хуже оказалось другое: копнув глубже, нашли, что форма регистрации принимала поле «роль». Любой человек мог зарегистрироваться как администратор. Просто указать в запросе нужное значение. Никакой проверки.
Это не тот баг, который можно отложить на следующий спринт.

Мы бросили всё и за 48 часов переписали три слоя: добавили фильтрацию в 25 маршрутов сервиса данных, закрыли уязвимость в сервисе интеграций (+365 строк) и убрали поле роли из регистрации. Написали постмортем на 210 строк. Кто читал выпуск про неделю 123, помнит наш «чек-лист новой двери». Так вот, на неделе 122 дверей без замков было ещё больше.
Оглядываясь из будущего: это был момент, когда безопасность перестала быть «тем, что мы сделаем потом». Постмортем, который мы написали в пятницу, стал первым документом в папке, которая через месяц превратилась в полноценную базу знаний по безопасности.
Голова вторая: кэширование, которое всё сломало
В четверг же (да, тот самый четверг) мы решили, что платформе нужно кэширование. Запросы к серверу летят на каждое действие, интерфейс подтормаживает, пользователи замечают. Решение казалось простым: добавить HTTP-заголовки Cache-Control, подключить серверный кэш. 632 строки нового кода, шесть файлов. Задеплоили.

И интерфейс сломался. Пользователь редактирует настройку, нажимает «Сохранить», а интерфейс показывает старое значение. Причина: браузер кэширует ответ и не делает новый запрос. Фикс: отключить браузерное кэширование для редактируемых данных.
Но дальше стало интереснее. Кнопки-переключатели начали «отскакивать» назад: нажимаешь, состояние меняется, через секунду возвращается обратно. Причина: конфликт между HTTP-кэшем и кэшем на стороне фронтенда. Два слоя кэширования не знали друг о друге и перезаписывали результат.

Пять коммитов за три дня, чтобы стабилизировать то, что должно было быть «простой оптимизацией». Итог: мы ввели профили кэширования. Данные, которые пользователь редактирует, получили профиль REALTIME (без кэша). Справочники и статика получили профиль CACHED. Каждый маршрут явно помечен, что исключает ситуацию «добавил маршрут и забыл про кэш».
Урок: кэширование никогда не бывает «просто добавить заголовки». Каждый слой кэша должен знать о существовании остальных.
Голова третья: авторизация-уроборос
Шесть коммитов за пять дней. Каждый исправлял бесконечный цикл перенаправления, и каждый раз цикл появлялся в новом месте.

Первый цикл: страница логина. Пользователь вводит данные, система авторизует, перенаправляет на главную, главная видит «не авторизован», перенаправляет обратно на логин. Бесконечно. Причина: охранник маршрута проверял токен до того, как тот сохранялся.
Второй цикл: после логина. Гостевой охранник и авторизационный охранник стартовали одновременно. Гостевой говорил «перейди на главную», авторизационный говорил «перейди на логин». Пинг-понг.
Третий цикл: обновление токена. Два запроса одновременно обнаруживали истёкший токен и оба пытались его обновить. Первый успевал, второй получал ошибку и разлогинивал пользователя.

В итоге мы переписали клиентскую библиотеку авторизации целиком и написали архитектурный документ на 712 строк. Не потому что планировали, а потому что без документа уже невозможно было удержать в голове все краевые случаи. Все случаи зафиксировали в трёх правилах: порядок операций, атомарность обновления, изоляция типов ошибок.
Четверг, который не заканчивался
4 декабря 2025 года. 45 коммитов за один день. Это больше, чем у некоторых проектов за месяц.

Утром один из разработчиков начал собирать админ-панель сервиса данных. За четыре с половиной часа (с 09:30 до 14:12) он построил полноценную панель управления: авторизация, отображение пользователей, управление ботами, кнопки запуска и остановки, фильтры, разграничение доступа. С нуля. Включая один коммит с опечаткой в сообщении, которую я не буду исправлять, потому что она передаёт скорость, с которой всё происходило.
Параллельно в этот же день: кризис безопасности, запуск кэширования (которое через час сломает интерфейс), мега-рефакторинг UX воронки на 6 740 строк и очередной раунд борьбы с авторизацией.
Коммиты этого дня заканчиваются в 01:22 ночи следующего.
Воронка продаж: залипание статусов
Между тушением пожаров мы строили воронку продаж с настраиваемыми этапами. Drag-and-drop для перемещения сделок между статусами, синхронизация с внешними CRM-системами, автоматический выбор специальных статусов.

Одна особенность оказалась коварной: флаг «специальный статус» (is_special) залипал после перетаскивания. Пользователь перемещал статус, и система отмечала его как специальный, хотя он таковым не являлся. Пять итераций фикса за одно воскресенье, чтобы наконец разобраться: порядок вызова React-хуков менялся в зависимости от направления перетаскивания. В одном направлении данные обновлялись корректно, а в другом стейт разрушался.
Пять итераций фиксов за одно воскресенье внутри рефакторинга на 6 740 строк.
Урок недели: принцип гидры
229 коммитов. Уязвимости, кэширование, авторизация, воронка, админ-панель, клиентские инциденты. И ощущение, что каждый фикс создаёт новую проблему.

Но это иллюзия. Проблемы не появлялись: они были там всегда. Просто до этой недели мы не копали достаточно глубоко. Кэш обнажил конфликт между слоями. Фикс авторизации вскрыл гонку состояний в токенах. Закрытие одной уязвимости показало пробоину в регистрации.
Если фикс вскрывает новую проблему, вы не делаете хуже. Вы наконец-то смотрите достаточно внимательно. Баги не живут поодиночке. Они гнездятся.
Чек-лист: инспекция соседей

Принцип гидры работает так: после крупного фикса задайте команде четыре вопроса.
Безопасность. Если мы чиним фильтрацию в одном сервисе, проверены ли остальные шестнадцать? Изолированы ли поля формы регистрации от прямого ввода?
Кэширование. Знают ли слои кэширования (браузер, прокси, клиент, серверный кэш) о правилах друг друга? Разделён ли мутирующий и статический контент?
Авторизация. Разделяем ли мы типы ошибок («токен истёк» vs «не авторизован») на уровне архитектуры?
Экосистема. Чините авторизацию? Посмотрите на регистрацию. Проверяйте соседние компоненты.
Если хотя бы на один вопрос ответ «нет» или «не знаю», начните с него. Именно там сидит следующая голова гидры.

Почему кэширование ломает интерфейс?
Потому что в современном веб-приложении обычно несколько слоёв кэша: браузер, прокси, фреймворк на клиенте, серверный кэш. Каждый слой работает по своим правилам. Когда пользователь редактирует данные, один слой обновляется, а другой продолжает отдавать старое значение. Решение: явная маркировка каждого маршрута профилем кэширования. Данные, которые пользователь может изменить, должны проходить мимо всех кэшей.
Что такое privilege escalation и почему это критично?
Привилегированная эскалация означает, что обычный пользователь может получить права администратора. В нашем случае форма регистрации принимала поле «роль» из запроса. Никакой проверки на стороне сервера. Любой, кто умел отправить HTTP-запрос вручную, мог стать администратором. Фикс занял одну строку (удаление поля из формы), но обнаружение потребовало целенаправленного аудита.
Как бороться с бесконечными циклами авторизации?
Три правила. Первое: токен должен сохраняться до того, как происходит перенаправление. Второе: обновление токена должно быть атомарной операцией: один запрос за раз, остальные ждут результата. Третье: разделяйте типы ошибок авторизации. «Токен истёк» (обнови) и «пользователь не авторизован» (перенаправь на логин) требуют разных действий. Если их не различать, система может бесконечно обновлять токен, который уже отозван.
Можно ли построить админ-панель за 4 часа?
Можно, если фреймворк берёт на себя рутину. В нашем случае разработчик использовал встроенные средства, которые автоматически генерируют формы по моделям данных. 50 моделей подключены за одну итерацию, CRUD-операции работают из коробки. Но это не «готовая админка». Это инструмент для разработчиков, который позволяет быстро проверить данные и запустить бота без интерфейса платформы.



