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

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

Разбор этой статьи

AI-подкаст Botseller229 коммитов и принцип гидры Botseller AI
0:00 / 0:00

Эту тему разобрали в подкасте. Слушай параллельно с чтением.

У древнегреческой гидры была особенность: отрубаешь одну голову, вырастают две. На неделе 122 мы столкнулись с инженерной версией этого мифа. Починили уязвимость безопасности и обнаружили за ней вторую. Добавили кэширование, оно сломало интерфейс. Исправили цикл авторизации, он появился в другом месте. Каждое решение вскрывало следующую проблему. 229 коммитов за неделю, и ощущение, что бежишь по эскалатору в обратную сторону.

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

Контекст: на пороге большой стройки

Начало декабря, продукт растёт. Боты работают, клиенты подключаются, появляются заказчики с десятками и сотнями сделок. Один из них продаёт недвижимость на Пхукете и генерирует столько запросов, что мы упираемся в лимиты внешней CRM-системы. Другой запрашивает тестовый чат, чтобы проверять ответы бота прямо из панели управления. Третьему нужна воронка продаж с настраиваемыми этапами.

Системная телеметрия: девять метрик аптайма, активности пользователей и нагрузки на ботов. Платформа растёт, но растут и скрытые напряжения в фундаменте

Мы строим всё это одновременно. И параллельно начинаем замечать, что фундамент, на котором стоит платформа, не так крепок, как казалось.

Голова первая: кризис безопасности

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

Кризис безопасности: RLS не работает + привилегированная эскалация через форму регистрации. Два слоя уязвимостей, обнаруженных одновременно

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

Это не тот баг, который можно отложить на следующий спринт.

Хирургическое вмешательство за 48 часов: добавили фильтрацию в 25 маршрутов сервиса данных, закрыли уязвимость в сервисе интеграций (+365 строк) и убрали поле роли из регистрации. Постмортем на 210 строк

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

Оглядываясь из будущего: это был момент, когда безопасность перестала быть «тем, что мы сделаем потом». Постмортем, который мы написали в пятницу, стал первым документом в папке, которая через месяц превратилась в полноценную базу знаний по безопасности.

Голова вторая: кэширование, которое всё сломало

В четверг же (да, тот самый четверг) мы решили, что платформе нужно кэширование. Запросы к серверу летят на каждое действие, интерфейс подтормаживает, пользователи замечают. Решение казалось простым: добавить HTTP-заголовки Cache-Control, подключить серверный кэш. 632 строки нового кода, шесть файлов. Задеплоили.

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

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

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

Матрица профилей кэширования: REALTIME (пользовательские настройки, кэш отключён полностью, запрос идёт в обход всех слоёв) vs CACHED (справочники, статика, стандартная маршрутизация через слои)

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

Урок: кэширование никогда не бывает «просто добавить заголовки». Каждый слой кэша должен знать о существовании остальных.

Голова третья: авторизация-уроборос

Шесть коммитов за пять дней. Каждый исправлял бесконечный цикл перенаправления, и каждый раз цикл появлялся в новом месте.

The Ouroboros Auth Loop: три цикла авторизации. Спешка (охранник проверяет токен до сохранения), Гонка (Guest Guard и Auth Guard стартуют одновременно), Коллизия (два запроса обновляют истёкший токен параллельно)

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

Второй цикл: после логина. Гостевой охранник и авторизационный охранник стартовали одновременно. Гостевой говорил «перейди на главную», авторизационный говорил «перейди на логин». Пинг-понг.

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

Архитектура стабильной авторизации: три правила. Порядок операций (токен сохраняется ДО перенаправления). Атомарность (обновление токена строго атомарно, остальные ждут). Изоляция ошибок (разделение «токен истёк» и «не авторизован»)

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

Четверг, который не заканчивался

4 декабря 2025 года. 45 коммитов за один день. Это больше, чем у некоторых проектов за месяц.

Бесконечный четверг: хронология 45 коммитов. Утро (09:30-14:12): админ-панель с нуля, 50 моделей, CRUD, управление ботами. День: кризис безопасности + запуск кэширования. Вечер: мега-рефакторинг UX воронки на 6 740 строк. 01:22 следующего дня: последний коммит

Утром один из разработчиков начал собирать админ-панель сервиса данных. За четыре с половиной часа (с 09:30 до 14:12) он построил полноценную панель управления: авторизация, отображение пользователей, управление ботами, кнопки запуска и остановки, фильтры, разграничение доступа. С нуля. Включая один коммит с опечаткой в сообщении, которую я не буду исправлять, потому что она передаёт скорость, с которой всё происходило.

Параллельно в этот же день: кризис безопасности, запуск кэширования (которое через час сломает интерфейс), мега-рефакторинг UX воронки на 6 740 строк и очередной раунд борьбы с авторизацией.

Коммиты этого дня заканчиваются в 01:22 ночи следующего.

Воронка продаж: залипание статусов

Между тушением пожаров мы строили воронку продаж с настраиваемыми этапами. Drag-and-drop для перемещения сделок между статусами, синхронизация с внешними CRM-системами, автоматический выбор специальных статусов.

Аномалия воронки: флаг is_special залипает при drag-and-drop. В одном направлении стейт обновляется корректно, в другом ломается. Причина: порядок вызова React-хуков зависел от направления перетаскивания

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

Пять итераций фиксов за одно воскресенье внутри рефакторинга на 6 740 строк.

Урок недели: принцип гидры

229 коммитов. Уязвимости, кэширование, авторизация, воронка, админ-панель, клиентские инциденты. И ощущение, что каждый фикс создаёт новую проблему.

Синтез: истинный принцип гидры. Иллюзия: каждый фикс ломает систему в новом месте. Реальность: проблемы были в архитектуре с самого начала, мы просто впервые начали копать достаточно глубоко. Кэш обнажил конфликт состояний. Фикс авторизации вскрыл гонку. Закрытие уязвимости показало пробоину в регистрации

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

Если фикс вскрывает новую проблему, вы не делаете хуже. Вы наконец-то смотрите достаточно внимательно. Баги не живут поодиночке. Они гнездятся.

Чек-лист: инспекция соседей

Чек-лист недели 122: четыре вопроса после крупного архитектурного фикса. Безопасность: проверены ли остальные шестнадцать сервисов? Кэширование: знают ли слои друг о друге? Авторизация: разделяем ли мы типы ошибок? Экосистема: проверяйте соседние компоненты

Принцип гидры работает так: после крупного фикса задайте команде четыре вопроса.

Безопасность. Если мы чиним фильтрацию в одном сервисе, проверены ли остальные шестнадцать? Изолированы ли поля формы регистрации от прямого ввода?

Кэширование. Знают ли слои кэширования (браузер, прокси, клиент, серверный кэш) о правилах друг друга? Разделён ли мутирующий и статический контент?

Авторизация. Разделяем ли мы типы ошибок («токен истёк» vs «не авторизован») на уровне архитектуры?

Экосистема. Чините авторизацию? Посмотрите на регистрацию. Проверяйте соседние компоненты.

Если хотя бы на один вопрос ответ «нет» или «не знаю», начните с него. Именно там сидит следующая голова гидры.

Конец записи. Статус: неделя 122 завершена. Уязвимости изолированы. Кэш стабилизирован. Инциденты этой недели стали фундаментом. Система готова к великому пивоту CRM

Почему кэширование ломает интерфейс?

Потому что в современном веб-приложении обычно несколько слоёв кэша: браузер, прокси, фреймворк на клиенте, серверный кэш. Каждый слой работает по своим правилам. Когда пользователь редактирует данные, один слой обновляется, а другой продолжает отдавать старое значение. Решение: явная маркировка каждого маршрута профилем кэширования. Данные, которые пользователь может изменить, должны проходить мимо всех кэшей.

Что такое privilege escalation и почему это критично?

Привилегированная эскалация означает, что обычный пользователь может получить права администратора. В нашем случае форма регистрации принимала поле «роль» из запроса. Никакой проверки на стороне сервера. Любой, кто умел отправить HTTP-запрос вручную, мог стать администратором. Фикс занял одну строку (удаление поля из формы), но обнаружение потребовало целенаправленного аудита.

Как бороться с бесконечными циклами авторизации?

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

Можно ли построить админ-панель за 4 часа?

Можно, если фреймворк берёт на себя рутину. В нашем случае разработчик использовал встроенные средства, которые автоматически генерируют формы по моделям данных. 50 моделей подключены за одну итерацию, CRUD-операции работают из коробки. Но это не «готовая админка». Это инструмент для разработчиков, который позволяет быстро проверить данные и запустить бота без интерфейса платформы.