Использование UUID для предотвращения атак методом перебора
Стандартной процедурой для схем баз данных является использование инкрементного первичного ключа для идентификации записей. Однако этот идентификатор также используется непосредственно в 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 часто являются самым простым решением. К другим решениям относятся:
Первичный ключ UUID: Аспект случайности UUID приводит к снижению производительности вставки в InnoDB, поскольку строки хранятся на диске отсортированными по первичному ключу. Вместо того чтобы добавлять данные в конец файла таблицы, вставки будут происходить по всему файлу для случайных данных.
Hashids: Числовое значение преобразуется в строку для ссылок, которые будет видеть пользователь. Это эффективный подход, но не каждый фреймворк поддерживает преобразование значения в другое представление при вставке ключа и обратное преобразование для поиска в базе данных. Кроме того, преобразование необходимо применять при копировании идентификаторов из URL для их поиска в базе данных вручную, а не прямой вставкой.
Snowflake: Микросервис генерирует сортируемые по времени идентификаторы, помещающиеся в столбец
bigint
. Эти идентификаторы не являются уникальными, поскольку не содержат случайности, и в стек добавляется сервис, от которого зависит каждыйINSERT
.ULID, Nano ID: Эти библиотеки генерируют специальные случайные строки для использования в качестве уникальных идентификаторов. Они занимают больше места, чем двоичные UUID, если не реализовано ручное преобразование двоичных данных. Поиск таких значений в базе данных вручную затруднён, поскольку двоичные данные необходимо копировать в средства управления базами данных и запросами.
В некоторых приложениях преимущества этих альтернативных подходов могут перевешивать указанные недостатки. Для некоторых фреймворков эти недостатки также отсутствуют благодаря автоматическому преобразованию данных или другим решениям. Однако все операции записи должно выполнять приложение, поскольку логика генерации и преобразования идентификаторов доступна только в коде приложения. Эффективные операции SQL типа INSERT ... SELECT
или другие подходы к сохранению данных без проксирования всех данных через приложение становятся недоступными. Выбранный подход к генерации идентификаторов препятствует многочисленным эффективным оптимизациям производительности, поскольку все операции сохранения данных должны обрабатываться приложением, а не базой данных.
Дополнительные ресурсы
- MySQL Blog: MySQL 8.0 UUID announcements describing new functions.
- PostgreSQL documentation: The space-efficient uuid data type.
- PostgreSQL documentation: gen_random_uuid() to create random UUID v4 IDs.
- Christian Emmers' Blog: The uuid_v4() function to add UUID v4 IDs to MySQL.