Использование UUID для предотвращения атак методом перебора

Источник: «UUIDs to prevent Enumeration Attacks»
В большинстве приложений ресурсы адресуются в URL по числовым инкрементным идентификаторам. Злоумышленник может легко увеличить идентификатор, чтобы перебрать все записи, собирая все доступные данные. Однако это легко предотвратить.

Стандартной процедурой для схем баз данных является использование инкрементного первичного ключа для идентификации записей. Однако этот идентификатор также используется непосредственно в URL-адресах приложений. Злоумышленник может вручную увеличить идентификатор, чтобы найти все существующие записи. Этот вектор атаки часто упускается из виду при разработке приложений, однако его можно легко модифицировать в существующих приложениях. Необходимо расширить каждую таблицу случайным столбцом UUID v4, заменив им числовой идентификатор в URL.

Использование

MySQL (требуется функция UUID v4)

ALTER TABLE users ADD COLUMN uuid char(36);
UPDATE users SET uuid = (SELECT uuid_v4());
ALTER TABLE users CHANGE COLUMN uuid uuid char(36) NOT NULL;
CREATE UNIQUE INDEX users_uuid ON users (uuid);

Функция UUID() в MySQL генерирует UUID v1, которые содержат временную составляющую, что делает их неравномерно распределёнными в течение коротких периодов времени. Мы можем определить собственную функцию для генерации UUID v4, которые являются случайными и поэтому распределены более равномерно.

Пользовательская функция uuid_v4()

Вот функции, с комментариями, поясняющими каждую группу:

CREATE FUNCTION uuid_v4() RETURNS CHAR(36)
BEGIN
-- 1-я группа состоит из 8 символов = 4 байта
SET @g1 = HEX(RANDOM_BYTES(4));

-- 2-я группа - 4 символа = 2 байта
SET @g2 = HEX(RANDOM_BYTES(2));

-- 3-я группа - 4 символа = 2 байта, начиная с a: 4
SET @g3 = CONCAT('4', SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3));

-- 4-я группа - 4 символа = 2 байта, начиная с a: 8, 9, A или B
SET @g4 = CONCAT(HEX(FLOOR(ASCII(RANDOM_BYTES(1)) / 64) + 8), SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3));

-- 1-я группа состоит из 12 символов = 6 байт
SET @g5 = HEX(RANDOM_BYTES(6));

RETURN LOWER(CONCAT(@g1, '-', @g2, '-', @g3, '-', @g4, '-', @g5));
END;

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

CREATE FUNCTION uuid_v4() RETURNS CHAR(36)
BEGIN
RETURN LOWER(CONCAT(
HEX(RANDOM_BYTES(4)),
'-', HEX(RANDOM_BYTES(2)),
'-4', SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3),
'-', HEX(FLOOR(ASCII(RANDOM_BYTES(1)) / 64) + 8), SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3),
'-', hex(RANDOM_BYTES(6))
));
END;

В данном случае используется RANDOM_BYTES() вместо RAND(), поскольку первая является недетерминированной и, следовательно, более криптографически безопасной, что в итоге приводит к меньшему количеству коллизий UUID.

Функция RANDOM_BYTES() была введена в MySQL v5.6.17 (2014) и в MariaDB v10.10.0 (23 Июня 2022).

PostgreSQL

ALTER TABLE users ADD COLUMN uuid uuid NOT NULL DEFAULT gen_random_uuid();
CREATE UNIQUE INDEX users_uuid ON users (uuid);

Подробное объяснение

Большинство приложений уязвимы для атак методом перебора из-за простого построения схемы базы данных. URL-адреса приложений содержат автоматически инкрементируемый первичный ключ записей базы данных, который может быть изменён. Злоумышленник может увеличить эти идентификаторы, чтобы легко перебрать все данные приложения. Эта атака опасна для любых приложений с некоторыми публичными ресурсами, доступными для всеобщего просмотра, например, профили пользователей в социальных сетях, заметки, которыми можно поделиться в менеджере заметок, и многое другое. В меньшей степени атака опасна для ресурсов, доступных только принадлежащему пользователю. Проверка подлинности не позволит любому злоумышленнику увидеть содержимое, но ценная информация все равно может быть собрана. Увеличение номера раскрывает количество зарегистрированных пользователей, заметок или других записей в базе данных. Периодически проверяя идентификатор, можно отслеживать увеличение клиентской базы или её использование любым конкурентом.

Устранить эту ошибку с раскрытием информации в существующих приложениях непросто. Большинство изменений носят повсеместный характер и требуют внесения многочисленных изменений во многие части приложения. Простым решением для существующих и новых планируемых приложений является использование схемы базы данных с числовым первичным ключом и добавление уникального случайного идентификатора для использования во внешних ссылках. Числовой ключ по-прежнему используется для идентификации записи в базе данных и на него ссылаются другие таблицы. Новый случайный ключ используется вместо первичного ключа в URL и формах, чтобы скрыть реальный первичный ключ от пользователей.

Самый простой способ — использовать для каждой записи случайный идентификатор UUID v4. Благодаря 128 случайным битам UUID невозможно угадать, коллизии маловероятны, и они хорошо поддерживаются в любом языке и фреймворке. PostgreSQL предоставляет эффективный для хранения тип uuid с функцией gen_random_uuid() для создания строк UUID v4. Для MySQL поддержка UUID гораздо сложнее: Реализован только стандарт UUID v1, который генерирует UUID по MAC-адресу сервера и текущему времени. Такие UUID не содержат никакой случайности и с большей вероятностью могут быть угаданы злоумышленниками. Случайные UUID должны генерироваться приложением или пользовательскими функциями базы данных. Не существует и эффективного формата хранения. UUID можно хранить в столбце char(36), занимающем не 16, а 36 байт. Если требования к занимаемому объёму более критичны, приходится выполнять ручные преобразования между строковым и двоичным форматами с помощью UUID_TO_BIN и BIN_TO_UUID.

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

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

Дополнительные ресурсы

Дополнительные материалы

Предыдущая Статья

PHP итераторы для перебора структур данных

Следующая Статья

Итерация файлов и каталогов в PHP