Недавно я проводил стресс-тест сервера мойе браузерной MMORPG: запустил 5000 синтетических ботов, порадовался результату. Казалось бы, победа, можно открывать шампанское.
Но затем пришло время суровой реальности. Код прошел через глубокий аудит серверной безопасности. В отчёте всплыло аж 45 уязвимостей.
Причём не абстрактных «теоретических брешей», а вполне конкретных, наглых дыр: от выдачи любого легендарного меча в один клик через F12 до падения всего онлайна из-за одного битого запроса на иконку зелья.
Собрал самые некоторые баги, которые пришлось фиксить ИИ.
1. Материализация топового шмота из воздуха
В игре есть стандартная механика: открыл сумку, кликнул по броне - персонаж её надел. Элементарно? Вроде да.
Пока мы не полезли в серверный код. Выяснилось прекрасное: сервер вообще не проверял, есть ли этот предмет у тебя в инвентаре.
Клиент просто отправлял пакет вида equip(itemID: 100500), а сервер добродушно отвечал: «О, классный клинок, надевай!». В итоге любой пытливый игрок мог открыть консоль браузера, написать две строчки с ID светящегося меча судного дня — и сервер послушно натягивал его на голого персонажа 1-го уровня. Никаких проверок на наличие, уровень или класс. Инвентарь Шрёдингера, где любая вещь уже твоя, стоит только попросить.
2. Телепортация через кнопку «Стоп»
Для обычного бега у нас стояла строгая античит-проверка: сервер непрерывно считал скорость и не давал разгоняться быстрее положенных 12 м/с. Любая попытка спидхака тут же каралась откатом персонажа назад. Сквозь горы и стены пробежать было невозможно.
Но читерская смекалка всегда найдет обходной путь. Лазейка нашлась в пакете остановки.
Когда игрок отпускает клавишу движения, клиент рапортует серверу: «Всё, я побегал и остановился вот в этих координатах: X, Y, Z». И сервер вместо сверки времени и пройденной дистанции просто брал эти координаты и перезаписывал позицию игрока в базе!
В итоге спидхак работал так: нажимаем шаг вперед, тут же жмем «Стоп» и отправляем координаты противоположного конца карты. Сервер пожимает плечами: «Ну ладно, стоишь так стоишь». Персонаж мгновенно материализовался сквозь любые скалы, стены и непроходимые реки.
3. Падение всего мира от одного скрытого нулевого байта
Чтобы быстро отдавать ассеты (текстуры, иконки, звуки), у нас крутился встроенный статик-сервер на Node.js.
Если в строке запроса к картинке подсунуть скрытый нулевой байт (%00, излюбленный инструмент пентестеров), низкоуровневая системная функция чтения файлов падает с фатальной ошибкой ядра.
А так как именно этот кусок не был завернут в аварийный перехват (try/catch), один кривой запрос ронял весь Node.js-процесс. Буквально: один школьник с модифицированным браузером мог кликнуть на сломанную ссылку - и весь сервер с сотнями игроков моментально падал наглухо.
4. Ультимативный щит: кик атакующего с сервера
Один из самых смешных багов. В формуле расчета шанса блока щитом разработчик банально забыл объявить одну переменную (кап шанса блока в 70%).
Почему этого не заметили на стресс-тестах? Потому что 5000 синтетических ботов самозабвенно месили друг друга голыми кулаками без экипировки. Нагрузочный тест прошел идеально, графики зеленые, разработчики довольны.
Но стоило живому тестеру надеть щит и принять первый удар от моба или другого игрока - движок спотыкался о необъявленную переменную, падал в необработанный эксепшн и… моментально обрывал сокет с атакующим!
Получилась идеальная тактическая защита: враг бьет тебя мечом по щиту - и его тут же выбрасывает с сервера в главное меню. Лучшая защита - это не броня, а бан атакующего.
5. Подмена сервера через ссылку
Для удобства локальной разработки клиент умел брать IP сервера прямо из URL-параметров (например, game.com/?server=127.0.0.1).
Беда была в том, что клиент не просто подключался к этому серверу без валидации домена, но и намертво сохранял его в localStorage браузера.
Сценарий атаки был элементарным: злоумышленник кидает в чат ссылку на игру со своим сервером-прокладкой. Жертва кликает, клиент сохраняет фишинговый адрес, и с этого момента даже при обычном входе на официальный сайт игра молча отправляла все сессионные токены, пароли и данные аккаунта прямо на сервер хакера.
Как мы с ИИшкой это лечили
Когда мы взялись закрывать эти дыры, вскрылся главный босс - монолитный серверный файл на 10 000 строк кода.
В нем перемешалось всё: боевая система, торговля, координаты, чат, база данных и инвентарь. Пытаешься починить надевание штанов - внезапно перестает работать покупка банок маны у торговца, потому что они делили один и тот же глобальный массив без нормальной изоляции.
Распил монолита на модули: Логику растащили по изолированным файлам. Трейд отдельно, банк/склад отдельно, боевой движок отдельно, инвентарь отдельно.
Тотальная паранойя в инвентаре (Zero Trust): Клиент больше вообще не оперирует ID предметов. Он может лишь попросить: «Сервер, надень то, что лежит у меня в слоте №3». Сервер сам лезет в БД, проверяет, есть ли там предмет, подходит ли уровень, совпадает ли класс персонажа. Сгенерировать вещь из воздуха больше нельзя технически.
Честный серверный стоп: Из пакета остановки координаты вырезали под корень. Теперь при получении команды «Стоп» сервер сам считает, где должен находиться игрок, учитывая направление вектора, пройденное время, скорость и геодату высот карты. Телепорты закрыты.
Вшитый белый список адресов: Чтение серверов из ссылок выпилили. Список игровых миров жестко зашит в клиенте, а localStorage принудительно санируется от любых левых хвостов.
Что в итоге
Сейчас фундамент безопасности и сетевой логики приведен в порядок (но это не точно). Архитектура стала модульной, дыры заколочены досками, а мы наконец-то можем вернуться к приятным вещам: настройке мобов, балансировке лута и расширению бесшовного мира.
Следить за разработкой, подкидывать идеи для мобов и участвовать в обсуждениях можно в нашем уютном Telegram-канале: 👉 t.me/ostrov_stali
Там же когда-нибудь )) будем раздавать ключи на пре-альфа тест (для тех, кто хочет лично прийти и попробовать сломать всё то, что мы только что починили). Первые смельчаки, которые выдержат бунт серверных кофемашин, получат памятный внутриигровой титул Первопроходцев.