Первый небольшой аудит

Обновлено 17 сентября 2026~26 мин чтения

В сентябре 2026 года один из пользователей прислал нам два документа: разбор того, как можно обмануть нашего Telegram-бота, и отчёт о проверке сайта и личного кабинета. Мы проверили каждый пункт, закрыли всё, что подтвердилось, и нашли по ходу ещё несколько слабых мест, о которых в отчётах не было ни слова.

ℹ️ О чём эта история

Здесь — что именно нашлось, как это работает и как мы защитились. Специальных знаний не нужно: каждый термин объясняется по ходу.

Зачем вообще нужны аудиты

Кто делает сервис, тот видит его изнутри. Он знает, как всё задумано, и именно поэтому перестаёт замечать, как это можно использовать не по назначению. Замок на двери лучше всех проверяет не хозяин, а тот, кто пытается её открыть.

Аудит безопасности — это взгляд со стороны. Кто-то смотрит на сервис глазами человека, который ищет лазейку, и записывает всё подозрительное. Бывает два вида такого взгляда:

Вид аудитаЧто видит проверяющийЧто может найти
Снаружитолько то, что доступно любому посетителю: сайт, бота, публичные файлыошибки, которыми может воспользоваться кто угодно
Изнутриещё и код, настройки серверов, базуошибки, которые снаружи пока никто не заметил

Оба наших документа — взгляд снаружи. У них есть важное свойство, о котором стоит помнить: находка аудита — это гипотеза, а не приговор. Проверяющий видит симптом и предполагает причину. Правильная она или нет, можно узнать только проверкой.

Поэтому с каждой находкой мы шли по одному и тому же пути:

  1. Проверить, есть ли проблема на самом деле: командой на живом сервисе, а не рассуждением.
  2. Понять, как она работает, и поискать соседние слабости того же рода.
  3. Исправить и снова проверить на живом сервисе.
  4. Защитить исправление тестом. Чтобы убедиться, что тест не пустышка, мы нарочно ломаем исправленный код и смотрим, заметит ли тест поломку. Если не заметил, тест переписываем.

Хитрый пример: как можно обмануть Telegram-бота

Первый документ интереснее второго. В нём нет ни одного взлома в привычном смысле: никто не подбирает пароли от серверов и не ищет ошибки в коде. Все четыре приёма используют то, как устроен сам Telegram, и то, что бот слишком доверяет людям, с которыми разговаривает.

Приём 1. Бота «сносят» чужим ником

Это самый неочевидный и самый опасный приём из четырёх. Он работает так:

  1. Покупается дешёвый аккаунт Telegram. В разборе названа цена около 20 рублей.
  2. В ник или имя аккаунта ставится то, что запрещено правилами Telegram: слова, связанные с насилием над детьми, мошенничеством, оскорблениями.
  3. С этого аккаунта пишется /start нашему боту.
  4. Бот вежливо здоровается и показывает профиль, а в шапке профиля пишет ник собеседника.
  5. На это сообщение бота с нескольких аккаунтов отправляются жалобы. В разборе говорится, что хватает пяти. Точный порог Telegram не публикует.

Хитрость в том, что запрещённые слова написал не бот, а человек. Но в чате они стоят внутри сообщения бота, и для того, кто разбирает жалобу, это выглядит так, будто бот сам рассылает запрещённое. Нашему боту до этого дня такая жалоба грозила: он действительно показывал ник в шапке профиля.

Как мы защитились:

Проверку нельзя обмануть привычными уловками: слово, разбитое точками («p.o.r.n»), написанное цифрами вместо букв («p0rn»), смесью латиницы и кириллицы или разделённое невидимым символом, она узнаёт так же, как обычное. Обычные имена, в том числе с инициалами вроде «A.Ivanov», проходят.

💡 Главный урок этого приёма

Всё, что бот пишет, он пишет от своего имени, даже если слова придумал кто-то другой. Поэтому любой чужой текст в сообщении бота — ник, имя, подпись — это риск, и показывать его стоит только тогда, когда без него не обойтись.

Приём 2. Бота приводят в чужую группу

Любого бота в Telegram можно добавить в любую группу, и владелец бота об этом не узнает. Бот, который написан для личной переписки, в группе ведёт себя так же, как в личке. Из этого получаются две неприятности.

Спам от имени бота. Бота добавляют в десятки групп и вызывают там командой /start@имя_бота. Участники видят незнакомого бота со ссылками и жалуются — на бота, а не на того, кто его привёл.

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

Мьют навсегда — неприятно, но не опасно. В чужой группе бот — такой же участник, как все остальные, и прав администратора ему никто не даёт. Значит, для него действуют те же правила: администратор группы может запретить ему писать, в том числе навсегда (Telegram считает ограничение вечным, если оно установлено больше чем на 366 дней). Но такой мьют живёт только внутри этой группы. В личных сообщениях и в любых других чатах бот продолжает работать как обычно, поэтому сам по себе мьют ему не вредит. Опасность этого приёма в другом — в спаме от имени бота и жалобах на него.

У всех трёх наших ботов — основного, поддержки и вакансий — добавление в группы было разрешено, а сам код бота не проверял, откуда пришло сообщение. Теперь защита стоит в три слоя:

СлойЧто делаетЗачем он нужен, если есть остальные
Настройка в BotFatherбота нельзя добавить в новую группуглавный замок на входе
Проверка в кодесообщения и нажатия кнопок из групп и каналов до бота не доходятнастройка не выгоняет бота из групп, куда его добавили раньше
Выход из группыесли бота всё же добавили, он сразу выходит — даже если его успели замьютитьне оставляет бота там, где его не ждали

Нажавший кнопку в группе увидит короткое «Бот работает только в личных сообщениях», а сам бот тут же покинет этот чат.

Приём 3. Невидимая буква и флуд командой /start

Многие боты защищаются от флуда просто: если одно и то же сообщение приходит слишком часто, лишние повторы отбрасываются. Эта защита обходится одним символом, которого не видно на экране.

Сообщение /start ㅤ выглядит как обычная команда с пробелом в конце. Но в конце стоит не пробел, а символ U+3164 — служебная «пустая» буква корейского алфавита. Для компьютера это буква, а не пробел. Поэтому фильтр повторов считает сообщение новым, а обработчик команд видит обычный /start с каким-то дополнительным словом и отвечает как на обычный старт.

СимволКак выглядитЧем является для компьютера
Обычный пробелпустое местопробел, его легко отрезать
U+3164 HANGUL FILLERпустое местобуква
U+2800 BRAILLE PATTERN BLANKпустое местосимвол
U+200B ZERO WIDTH SPACEничегослужебный невидимый знак

У нашего бота фильтра повторов по тексту не было, поэтому и обходить было нечего. Но проверка этого приёма привела к находке поважнее. Мы посмотрели, как часто люди отправляют боту /start, и увидели две совершенно разные картины:

Команд /start от одного аккаунта за минуту Обычный человек максимум за 30 дней Аккаунт из флуда 16 сентября, 12:42 5 ≈ 50 за 31 секунду
Рис. 1. Как часто присылают /start: обычный человек и флуд 16 сентября

Почему это важно, если бот всё равно ответил? У ответов бота есть общий лимит со стороны Telegram. Когда бот его превышает, Telegram заставляет его ждать, и в это время медленнее отвечают все — в том числе люди, которые просто хотели открыть свой профиль. К тому же каждый /start — это запрос к нашей базе за данными профиля.

Как мы защитились:

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

Приём 4. Бесконечный пробный период через группу

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

Работает это из-за путаницы между двумя номерами. В каждом сообщении Telegram передаёт боту два разных идентификатора:

НомерЧто этоВ личной перепискеВ группе
Номер отправителякто написалномер человеканомер человека
Номер чатагде написалитот же номер человеканомер группы, у каждой группы свой

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

Наш бот узнаёт человека только по номеру отправителя. Для проверки мы посмотрели базу: среди 7 918 человек нет ни одного номера группы или служебного аккаунта Telegram. Этот приём у нас не работал никогда, а после защиты от групп из приёма 2 бот в группе и вовсе не ответит.

Итог по боту

ПриёмРаботал ли у насЧто сделали
Бота «сносят» чужим никомда, ник был в шапке профиляубрали ник из шапки, имя в подарке проверяется
Бота приводят в чужую группуда, все три бота можно было добавитьзапрет в BotFather, проверка в коде, выход из группы
Невидимая буква против антифлуданет, но нашёлся флуд 16 сентябрялимит /start на человека
Пробный период через группунетничего не требовалось

Аудит сайта и личного кабинета

Второй документ устроен иначе. Это отчёт, составленный по публичным файлам сайта и кабинета: 12 находок, из них 6 «высокой» важности, и общий индекс защищённости 52 из 100.

Мы проверили каждую находку на живом сайте. Подтвердилась одна, и та низкой важности. Большая часть остальных оказалась предположениями вида «если сервер не делает X, то возможна атака Y» — а сервер X делает. Зато, пока мы проверяли, нашлись настоящие слабости, о которых в отчёте прямо не говорилось. Два пункта отчёта указывали в правильную сторону, хотя описанный в них сценарий был другим.

Что не подтвердилось

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

В отчётеЧто оказалось
Нет защитных заголовков: HSTS, запрета встраивания, запрета «угадывания» типа файловвсе эти заголовки уже были; не хватало только одной опции у HSTS, о ней ниже
Список скрытых разделов в коде сайта ведёт к админкесписок действительно виден, но это нормально для таких сайтов; права проверяет сервер, и постороннему он отказывает
Хранимая XSS-атака через комментариитекст комментариев выводится как текст, а не как код, а политика безопасности сайта вдобавок запрещает запускать чужие скрипты
Утечка адресов серверов через карточки нодадресов в ответе нет, список серверов виден только после входа
Подмена запросов сервера через «диагностику соединения»пользователь не передаёт серверу никаких адресов, подменять нечего
Слабая секретная фраза для входафразу из 8 слов создаёт сервер надёжным генератором случайных чисел, и сейчас этот способ входа выключен
Повторное использование задачи «доказательства работы»каждая задача одноразовая и подписана сервером
Устаревшие версии TLS, открытые файлы настроек и бэкапыTLS 1.0 и 1.1 отключены, служебные файлы и бэкапы наружу не отдаются

Настоящая находка 1: чужой вход через Google

Представьте гостиницу, где ключ-карту выдают по предъявлению брони. Злоумышленник бронирует номер на своё имя, получает карту и незаметно подкладывает её вам. Вы заходите в номер, раскладываете вещи — и не замечаете, что номер его, а не ваш.

С входом через Google было похоже. Когда вы нажимаете «Войти через Google», сайт отправляет вас к Google, а Google возвращает вас обратно по особой ссылке. Эта ссылка подписана нашим сервером, поэтому подделать её нельзя. Но она не была привязана к браузеру, в котором вход начинали.

Значит, злоумышленник мог начать вход со своим Google-аккаунтом, остановиться на последнем шаге и отправить ссылку возврата вам. Вы открываете ссылку — и оказываетесь в его кабинете, не замечая подмены. Всё, что вы там сделаете, например привяжете свой Telegram, окажется в его аккаунте. Такая атака называется CSRF-входом (подделкой межсайтового запроса при входе).

Как было Злоумышленник начинает вход своим Google Ссылка возврата отправляет её вам Вы в его кабинете сайт проверил только подпись ссылки Как стало Злоумышленник метка остаётся в его браузере Ссылка возврата открывается у вас Вход не состоится в вашем браузере нет метки этого входа Метка кладётся в браузер в момент нажатия «Войти через Google», и чужой браузер её подделать не может.
Рис. 2. Чужая ссылка возврата: как было и как стало

Как мы защитились. Теперь в момент нажатия «Войти через Google» сайт кладёт в ваш браузер одноразовую метку, а при возвращении проверяет, что метка на месте и совпадает со ссылкой. Чужая ссылка в вашем браузере метки не найдёт, и вход не состоится. Меток хранится несколько, поэтому вход не ломается, даже если вы нажали кнопку дважды или открыли вход в двух вкладках.

Настоящая находка 2: подбор кода входа из Telegram

В кабинет можно войти кодом, который присылает бот. У этого кода есть особенность: сайт не знает заранее, чей это код. Человек просто вводит код, а сайт ищет его среди всех кодов, выданных за последние 5 минут.

Значит, тот, кто перебирает коды наугад, угадывает не код конкретного человека, а любой из кодов, выданных в эти минуты. Кто-то войдёт в кабинет — и перебирающий окажется в его аккаунте. Сайт ограничивал число попыток с одного адреса, но у злоумышленника с тысячами адресов этот барьер слабее.

Код состоял из 6 символов, и каждый символ — один из 32 вариантов (латинские буквы и цифры без похожих, вроде O и 0). Мы увеличили длину до 8 символов. Вот что это меняет:

Длина кодаСколько всего вариантовСколько в среднем перебирать до первого попадания*
6 символов1 073 741 824около 2,5 суток
8 символов1 099 511 627 776около 7 лет

*Если бы кто-то смог проверять 1000 кодов в секунду, а живых кодов одновременно было бы 5. В реальности сайт такой скорости не позволит — это оценка сверху.

Два лишних символа делают перебор примерно в тысячу раз дольше. Кроме того, мы завели тревогу: если неверных кодов вдруг становится заметно больше обычного, мы узнаём об этом сразу. Обычно их единицы — это опечатки.

Панель управления почтой

Почта Akonit работает на своём почтовом сервере, и у этого сервера есть веб-панель управления ящиками. Проверяя отчёт, мы заметили, что панель открыта всему интернету. В тот же день в логах нашлись попытки войти в неё под разными именами — они упёрлись во встроенный лимит попыток, и ни одна не удалась.

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

Мелочи, которые тоже поправили

Почему нашли так мало

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

Две настоящие находки из этого документа устроены иначе: код входа не был привязан к человеку, а ссылка возврата от Google — к браузеру. Такие ошибки не видны по списку типовых проверок, их находишь, только разобравшись, как именно работает конкретный вход. Именно поэтому взгляд со стороны всё равно нужен.

Мы проверяем себя постоянно

Проверка встроена в каждую доработку. Прежде чем выкатить изменение, мы пишем к нему тест и нарочно ломаем код, чтобы убедиться, что тест поломку заметит. Кроме того, мы регулярно смотрим на сервис глазами злоумышленника — отдельно на каждую его часть:

Часть сервисаЧто проверяем
Сайт и личный кабинетможно ли увидеть или изменить чужие данные, подменив номер в запросе; можно ли заставить сайт запустить чужой код через комментарий, карточку или ссылку; что будет, если загрузить «неправильную» картинку
Бэкендзакрыты ли служебные запросы ключом; не попадают ли пароли и ключи в журналы; не выполнит ли сервер запрос туда, куда его подтолкнул пользователь
Серверы VPNоткрыты ли наружу служебные порты; можно ли через сам VPN добраться до внутренних адресов сервера и нашей служебной сети
Боткакие чужие слова он может произнести от своего имени; можно ли получать бонусы по кругу; что будет при флуде

Вот что за последние месяцы мы нашли сами, без внешних отчётов:

КогдаЧто нашлиЧто сделали
Июль 2026ссылка в совете о хостинге могла запустить скрипт у администратора, который её откроеттеперь принимаются только обычные ссылки http и https
Июль 2026маленький файл картинки с огромным разрешением мог занять всю память сервера при загрузкеразмер картинки проверяется до того, как она развёрнута в памяти
Июль 2026вся внешняя часть сервиса разом: права доступа, запросы к базе, загрузка файлов, ссылкинарушений не нашлось; на сайт добавили политику безопасности, которая запрещает запускать чужие скрипты

Защита от SQL-инъекций

Сайт хранит данные в базе и общается с ней на языке SQL. Запрос к базе выглядит как команда: «найди пользователя с таким-то адресом почты». SQL-инъекция — это когда злоумышленник вместо адреса почты вписывает кусок другой команды, например «…и заодно выдай всех пользователей». Если сервер склеивает команду с тем, что ввёл человек, база не отличит одно от другого и выполнит всё целиком.

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

Мы работаем по второму способу. Текст запроса и данные пользователя всегда уходят в базу раздельно: сначала команда с пустыми местами, потом отдельно — чем их заполнить. База подставляет данные именно как данные и никогда не выполняет их как команду, даже если там написано что-то похожее на SQL. Так устроены все запросы: в личном кабинете, в бэкенде и в панели управления.

Бывает, что часть самого запроса собирает программа, например выбирает, по какому полю сортировать список. Тогда эта часть берётся из списка, записанного в коде, и никогда из того, что прислал пользователь. В июле мы отдельно просмотрели все запросы личного кабинета — ни одного места, где данные пользователя попадали бы в текст команды, не нашлось.

Подбор паролей и невидимая капча

Самая частая атака на вход — подбор: перебирать пароли, пока какой-нибудь не подойдёт. Паролей у нас нет вовсе. В личный кабинет входят через Google, кодом из Telegram-бота или кодом из письма. Подбирать можно только код входа, а он живёт несколько минут.

Раньше, когда вход был по паролю, его защищала обычная капча — картинка с искажёнными буквами. Сейчас там, где бот мог бы действовать массово, стоит невидимая капча. Это запрос кода на почту — с него же создаётся новый аккаунт. Браузер незаметно для человека решает небольшую математическую задачу, секунду-две. Человеку это ничего не стоит, а тому, кто хочет отправить тысячу запросов, придётся решить тысячу задач. Каждая задача одноразовая и подписана сервером, поэтому решить одну и повторять её не получится. Заодно невидимая капча не даёт использовать наш сайт для рассылки писем на чужие адреса.

Сам перебор кода сдерживают лимиты, о которых ниже. Главный из них считает попытки не по адресу злоумышленника, а по почтовому ящику, который пытаются взломать: сколько бы у злоумышленника ни было адресов, число попыток на один ящик всё равно ограничено.

Лимиты — везде

Лимит — это правило вида «не больше стольких-то действий за такое-то время». Он не мешает живому человеку и делает бесполезным всё, что держится на количестве: перебор, флуд, массовые регистрации, спам.

Главное в лимитах — считать правильную вещь. Если считать только по адресу, злоумышленник с тысячей адресов получит в тысячу раз больше попыток. Поэтому у нас лимиты стоят слоями, и каждый слой считает своё:

1 Веб-сервер ограничено число запросов входа с одного адреса 2 Невидимая капча перед отправкой письма браузер решает задачу 3 Адрес и подсеть много адресов из одной сети считаются вместе 4 Почтовый ящик ограничены и письма с кодом, и попытки ввести код 5 База данных данные идут отдельно от текста запроса Лишние запросы отсекаются на самом раннем шаге, где это дешевле всего. Лимит на почтовый ящик нельзя обойти сменой адресов: он считает не нападающего, а цель.
Рис. 3. Через что проходит попытка войти по почте

Вот какие лимиты стоят в разных частях сервиса:

ГдеЧто ограничено
Сеть и веб-серверчисло запросов входа и обычных запросов с одного адреса
Вход в кабинетпопытки ввести код — с одного адреса, с одной подсети и на один почтовый ящик; письма с кодом — на один ящик
Тяжёлые операцииу сервера ограниченное число «мест» для дорогих вычислений и отправки писем: лишние запросы получают «попробуйте позже», а сервер не падает
Анонимные запросыу запросов без входа своя доля ресурсов: толпа незнакомцев не может вытеснить тех, кто уже вошёл
Комментарии, предложения, жалобы, фоточисло публикаций на человека и на адрес
Ботсколько раз человек может отправить /start и сколько вложений прислать в поддержку
Поддержка в кабинетечисло сообщений в поддержку

Лимит сам по себе не последняя линия. Там, где перебор возможен в принципе, стоит ещё и тревога: если кто-то упирается в лимиты или вводит подозрительно много неверных кодов, мы узнаём об этом сразу, а не из жалоб.

ℹ️ Это не значит, что уязвимостей нет

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

💡 Нашли уязвимость?

Напишите на security@akonit.tech или в бот поддержки @akonit_support_bot. Мы проверим каждую находку, даже если она окажется гипотезой, — так, как проверили эти.

Что стоит запомнить

Источники

Вопросы по статье — в обсуждении выше, в поддержку из бота или из кабинета.

Akonit — технологии приватного доступа в интернет.