Logo

Как Chatify выдал клиентскую базу интернет-провайдера

★ Утечка базы данных из-за Chatify для Laravel.

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

Уведомление о малом остатке средств

В один из дней вместо уведомления была выведена ошибка, оказалось провайдер обновил сайт и полностью переделал личный кабинет. Среди прочего была добавлена возможность написать в техническую поддержку прямо из личного кабинета. Но вместо собственной реализации простого чата, программисты решили воспользоваться готовым решением в виде Chatify. Данный чат работает поверх PHP фреймворка Laravel. Внутри чата используется открытое API которое позволяет получать информацию о пользователях, банальное – получить адрес изображения аватара.

<?

Route::post('/idInfo', 'MessagesController@idFetchData');

// ...

public function idFetchData(Request $request)
{
    $favorite = Chatify::inFavorite($request['id']);
    $fetch = User::where('id', $request['id'])->first();
    if($fetch){
        $userAvatar = Chatify::getUserWithAvatar($fetch)->avatar;
    }
    return Response::json([
        'favorite' => $favorite,
        'fetch' => $fetch ?? null,
        'user_avatar' => $userAvatar ?? null,
    ]);
}

Здесь важно то, что информация о пользователе возвращается средствами фреймворка Laravel (выделенная строка в коде), соответственно берётся запись из общей таблицы, а не из какой-то отдельной заведенной специально для нужд чата. Причем информация выводится для абсолютно любого пользователя по его идентификатору (id) задаваемому в запросе, что логично для чат-системы которой может потребоваться отрисовать данные для совершенно различных пользователей.

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

Запрос подтвердил ожидания, зарегистрированному аккаунту полностью доступна вся внутренняя клиентская база, информация выдается для любого идентификатора:

Чтобы сократить вывод, для jq здесь указан простой скрипт, он обходит все поля и удаляет те из них, которые содержат “пустые” значения.

walk(if type == "object" then with_entries(if .value == 0 or .value == "" or .value == null then empty else . end) else . end )

Информация о пользователе

По запросу из базы подтягивается вся доступна информация о клиенте: ФИО, адрес, телефон, для ряда клиентов паспортные данные и адрес электронной почты, IP-адрес, коммутатор и номер порта. Кроме того доступны рабочие заметки которые провайдер ведёт для каждого клиента, например можно найти данные аутентификации на роутере пользователя для удалённого доступа.

Но особое удивление вызвал тот факт, что в базе пароли клиентов хранятся в неизменном виде и открытым текстом. Это одна из самых базовых уязвимостей CWE-256 («Хранение пароля в незашифрованном виде»). Чем опасна такая утечка? Понятное дело, можно получить доступ к любому аккаунту. В данном случае это не так страшно, максимум можно подключить жертве ненужные услуги или сменить тариф. Но что более важно, возможна ситуация когда пользователь использует данный пароль и для других сервисов, например для электронной почты, для входа в социальные сети, банки, систему.

Чтобы подобное предотвратить, ни в коем случае нельзя хранить пароли, ни в открытом, ни в зашифрованном виде. Хранить нужно получаемые от паролей хеши, причем просто хранить голый хеш так же не подходит, иначе пароль можно будет подобрать перебором, либо даже найти по готовым таблицам. Поэтому принято не просто брать хеш от пароля, а добавлять прежде к паролю случайное значение (соль). Таким образом к каждому пользователю придется индивидуально подбирать пароль, невозможно будет воспользоваться таблицами, невозможно будет определить пользователей у которых одинаковые пароли. Одним из лучших алгоритмов для этого на текущий день считается Argon2 (RFC 9106) Бирюкова, Дину и Ховратовича.

Но и это недостаточная защита. Если был получен доступ к базе данных (как в описываемом случае), то получен и доступ ко всем солям, а потому вероятность раскрытия паролей сохраняется. Поэтому пароли дополнительно рекомендуется не только “солить”, но и “перчить” (от простого добавления значения как соли, до использования имитовставки). В отличие от соли, которая уникальная для каждого пароля и хранится в базе, значение перца едино для всех паролей, но оно никогда не хранится в базе. Тем самым даже если будет получен доступ к БД, значение перца остаётся в тайне. Задача хранения перца отдельный вопрос, в идеале это всё должно быть аппаратное решение, но в реальной жизни подобное встречается редко и значение просто прописывают в коде. Однако куда более верным решением будет разместить его в переменной окружения и считывать во время выполнения, чем хранить значение прописанное непосредственно внутри кода (обычно к листингам имеет доступ куда больше людей, чем к серверу, да и утечки кода встречаются не так уж и редко). Само же значение в программе следует хранить в защищённой области памяти, чтобы если даже злоумышленник проник на сервер, он не мог его просто так прочесть из памяти.

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

Уязвимость была обнаружена ночью, через два дня после запуска нового личного кабинета, с утра провайдер был проинформирован и уже через 15 минут программисты перекрыли утечку.

В качестве благодарности провайдер на счёт добавил средств эквивалентных 20 месяцам оплаты по действующему тарифу.