Розбираємо UUID у всьому його різноманітті

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер і технічний письменник. Хочу розповісти вам про такий формат, як UUID, та сферу його застосування. У цій статті я опишу різні версії UUID, його підтримку в базах даних, операційних системах, мовах програмування і також поділюся результатами тестування швидкодії генерації UUID. Сподіваюся, що ця стаття буде корисна для всіх, хто хоче більше дізнатися про сучасні тенденції роботи з даними та створення унікальних ідентифікаторів.

Що таке UUID

Коли я тільки-но почав працювати з Java, то для одного із завдань мені знадобився унікальний ідентифікатор, який можна було отримати з Java. Я дізнався, що є стандартний клас UUID і досить простий код для генерації такого унікального ідентифікатора:

UUID id = UUID.randomUUID();

На довгі роки цей код був моїм основним досвідом роботи з UUID, поки 2023-го року я не почув про те, що вийшла сьома версія специфікації UUID. Я вирішив детальніше вникнути в це поняття і те, як його варто застосувати.

Отже, UUID (Universally Unique Identifier) ​​був придуманий у 1980-ті роки компанією Apollo Comput-er як 64-бітовий ідентифікатор, який повинен бути глобально унікальним. Вже тоді назріла проблема з чисельними ідентифікаторами баз даних, які не підходили на випадок розподіленої системи. До нового формату було пред’явлено такі вимоги:

  1. Відсутність чи мінімальна ймовірність колізій, тобто глобальна унікальність.
  2. Відсутність необхідності в централізованому сервері для генерації таких значень, тобто можливість генерації їх розподілено.

Набагато пізніше з’явилися ще дві вимоги:

  1. Безпека (неможливість за UUID відстежити людину або систему, де був згенерований UUID).
  2. Монотонність (кожне наступне значення має бути за попереднім при лексикографічному сортуванні).

Але спочатку були лише перші дві вимоги, при чому головне це глобальна унікальність. Невипадково компанія Microsoft, яка також почала його використовувати, перейменувала його в GUID (Globally Unique Identifier). Потім він з’явився як специфікація в Open Software Foundation (OSF), після чого в 2005-му році з’явилася специфікація UUID від організації IETF (Internet Engineering Task Force), яку зараз всі й використовують, але вже зі 128-бітним значенням як ідентифікатор.

На сьогодні існує 8 версій формату UUID, при чому версія вказується в самому значенні й під неї відводиться 4 біти.

Версія 1

UUID у версії 1 містить дві складові:

  1. Кількість 100-наносекундних інтервалів після 15 жовтня 1582 (60 біт) в часовому поясі UTC. Ви також можете генерувати це значення у своєму локальному часі, але це не рекомендується, щоб уникнути колізій при генерації даних у різних часових поясах. Цікаво, що якщо ваш комп’ютер не може вимірювати час у наносекундах, допускається використання випадкового значення замість часу.
  2. MAC-адреса комп’ютера, де генерується UUID (48 біт). Якщо у комп’ютера кілька MAC-адрес, то вибирається будь-яка, якщо жодної, замість неї використовується випадковим чином згенерована послідовність символів. Така ситуація може бути, якщо ви хочете згенерувати UUID у браузері, який не має доступу до MAC-адреси.

Такий підхід є найшвидшим у використанні. Достатньо було один раз отримати MAC-адресу (яка не змінювалася) і потім просто додавати поточний час. Але водночас він легко дозволяв визначити MAC-адресу комп’ютера та час створення UUID, що могли використовувати зловмисники для своїх атак. Є майже детективна історія, як Девід Сміт, автор вірусу «Melissa», був спійманий у 1999 році, оскільки залишив UUID зі своєю MAC-адресою. В результаті поява більш жорстких вимог до безпеки зробила цю версію менш популярною. Крім того, через широке застосування контейнерів і віртуальних мереж MAC-адреса могла бути неунікальною, що призводило до можливих колізій.

Ще одна потенційна небезпека цього підходу (і всіх, що беруть поточний час) — це можливість переведення поточного часу назад, що у майбутньому може призвести до колізій.

Загалом підхід, закладений у версію 1, був дуже привабливим. Зрозуміло, що для забезпечення глобальної унікальності потрібно додавати до UUID довільне число. Але навіщо це робити, якщо у нас вже є MAC-адреса, яка і так є унікальною? Але вимоги до безпеки та розвиток інфраструктури мереж призвели до того, що зараз ця версія використовується набагато менше, ніж раніше.

Версія 2

Версія 2 називається «DCE Security UUID», тому що її реалізація ніяк не вказана у специфікації RFC 4122, але вказується в старішій специфікації DCE Authentication and Security Service та враховує особливості UNIX-систем (стандарту POSIX). UUID у ній містить:

  1. MAC-адреса комп’ютера.
  2. Час у 7-хвилинних інтервалах.
  3. Ідентифікатор користувача або його групи на локальному комп’ютері.

Важко сказати, чим керувалися автори цієї специфікації, зменшуючи кількість біт на часовий інтервал, але тепер можна було згенерувати лише (!) 64 унікальних UUID протягом 7-хвилинного інтервалу часу. Порівняйте це з 16384 унікальними значеннями на кожні 100 наносекунд для версії 1. Тому дана версія зараз практично не використовується.

Версія 3

Одним із недоліків версії 1 було те, що вона була прив’язана до поточного часу. Таким чином, якщо ви використовували згенерований UUID як shard key у вашому кластері без його хешування, деякі вузли в кластері могли містити набагато більше даних, ніж інші. Ці та інші недоліки створили передумови для створення нового алгоритму генерації UUD, де використовувалося б хешування даних. Але не просто хешування, а хеширування, яке б не дозволяло б отримати вихідне значення. У версії 3 (named-based) для цієї мети був обраний алгоритм шифрування MD5, незважаючи на те, що ще у 2004-му році було виявлено, що він схильний до колізій.

У цій версії замість прив’язки до локальної MAC-адреси або доменної групи (як у версії 2) вибрали концепцію використання пари namespace і name. Що таке namespace? Специфікація говорить, що це має бути деяке фіксоване значення, прив’язане до вашої системи (теж UUID), і пропонує 4 стандартні значення:

  • DNS: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
  • URL: 6ba7b811-9dad-11d1-80b4-00c04fd430c8
  • OID: 6ba7b812-9dad-11d1-80b4-00c04fd430c8
  • X.500 DN: 6ba7b814-9dad-11d1-80b4-00c04fd430c8

Name — це певний рядок, який ідентифікує той об’єкт, для якого ви генеруєте UUID. Таким чином під час генерації UUID Namespace і Name конкатенуються, а потім над отриманим рядком виконується хешування MD5.

Уявімо, що у вас є сайт www.sample.com і на ньому ресурс із відносним URL/products/1. Для створення UUID ви використовуєте функцію uuid_v5. Тоді процес створення ідентифікатора для вашого ресурсу може бути двоетапним. Спочатку ви обчислюєте перший UUID на базі стандартного DNS UUID та домену сайту:

firstUuid = uuid_v5 (DNS_UUD, «www.sample.com»)

А на другому етапі ви замість Namespace підставляєте перший UUD, а замість Name — адресу ресурсу:

secondUuid = uuid_v5 (firstUuid, «/product/1»)

Якщо вихідний Namespace був унікальним (у цьому разі — firstUuid), це гарантує відсутність колізій. Головний мінус такого підходу — це вимога до незмінності вихідного Namespace (URL, DNS, OID). Якщо ж він таки зміниться, то повторна генерація UUID поверне значення, відмінне від існуючого (у тій же базі даних) і може призвести до втрати цілісності ваших даних.

Відносний мінус такого підходу — у того ресурсу, для якого ви генеруєте UUID, має бути унікальний і immutable Name (або URI) в межах вашої системи.

Версія 4

Цей алгоритм є найпопулярнішим зараз (за деякими оцінками, до 80-90% від усіх версій UUID) і в ньому UUID заповнюється лише випадковими значеннями.

Таким чином, цей підхід варто використовувати, якщо вам потрібен глобальний унікальний ідентифікатор, який не прив’язаний до конфігурації вашого комп’ютера або до поточного часу. Щоправда, ймовірність появи колізій залежить від того, наскільки випадковими будуть значення, отримані від використовуваного генератора випадкових чисел.

Версія 5

Згодом в алгоритмі MD5 знайшли таку кількість проблем з безпекою його роботи, що було ухвалено рішення на основі версії 3 додати версію 5, де замість MD5 для хешування використовувати більш стійкий алгоритм SHA-1. Щодо швидкодії, то SHA-1 дозволяє процесору розпаралелити частину своєї роботи, тому дослідження показують, що на сучасних комп’ютерах генерація SHA-1 хешу обчислюється швидше, ніж MD5.

Наскільки популярні версії 3 та 5? У 2021 році було проведено дослідження серед JavaScript-репозиторіїв на GitHub, і виявилося, що вони використовуються лише в 1% від усіх проєктів, де була генерація UUID (а версія 4 — 77%).

Версія 6

Версія 6 практично повністю збігається з версією 1, за винятком того, що там поміняли місцями верхні та нижні біти в часі, що дозволило сортувати UUID значення за датою створення.

Версія 7

У 2016 році відбулася визначна подія. Розробник Алізайн Фіраста оголосив про створення нового формату ULID (Universally-Unique, Lexicographically-Sortable Identifier), специфікацію якого він і опублікував. Головними його особливостями були:

  1. Повна сумісність із UUID (ті ж 128 біт).
  2. Можливість перетворення на 26-символьний рядок (у UUID 36 символів). Це досягається шляхом того, що використовується кодування base32 з 5 бітами на кожен символ.
  3. Відсутність спеціальних символів, що дає можливість використання його в URL.
  4. Монотонність значень та можливість використання його при сортуванні.

Остання особливість найбільш важлива, оскільки вона була відсутня в UUID. На цей час реалізація ULID є практично для всіх популярних мов програмування. Який формат даних в ULID? Спочатку йде 48 біт (час у мілісекундах з початку епохи Unix), потім 80 біт — випадкові символи.

Популярність нового формату призвела до того, що IT-спільнота задумалася про створення нової версії UUID, яка також підтримувала б монотонність. І ось у 2023 нарешті вийшла версія 7, яка пропонує наступний формат:

  1. Кількість мілісекунд, що пройшли після 1 січня 1970 року (48 біт).
  2. Випадкове значення.

Використання на початку рядка часу в мілісекундах дозволяє отримати UUID-значення, відсортовані за зростанням, що якраз і потрібно для використання такого ключа в базах даних. Водночас з метою дотримання безпеки ви можете зрушити вперед або назад цей час, щоб зловмисники не могли дізнатися точний час створення UUID.

Лічильник випадкових значень ініціалізується при кожному створенні UUID, що дозволяє уникнути колізій, якщо час буде одним і тим же.

Версія 8

Ця версія ще називається vendor-specific, тому що вона лише вимагає наявності в UUID обов’язкових полів версія/variant, тоді як сама генерація значень залишається за вендором.

Підтримка у базах даних

Якщо ви використовуєте один сервер БД або один сервер з репліками, достатньо використовувати автоінкрементне поле або лічильник для генерації первинного ключа. Якщо ж у вас кластер, який може розташовуватися в різних регіонах, то необхідно забезпечити глобальну унікальність первинного ключа. І тут може стати в нагоді UUID.

MongoDB містить функцію UUID(), яка повертає UUID-об’єкт версії 4. Чи варто використовувати її замість дефолтного 96-бітного формату ObjectId? Результати тестів показують, що використання UUID разом з ObjectId уповільнює продуктивність при вставці в 2-3 рази.

MySQL також містить функцію UUID(), яка повертає значення версії 1. Цікаво, що ця реалізація функції залежить від платформи. На FreeBSD, Linux і Windows спочатку йде MAC-адреса (як і має бути), а ось на інших платформах (Solaris, MacOS) генерується 48-бітове випадкове число.

У Postgres 13 і пізніших версій цих цілей йде функція gen_random_uuid(), яка генерує значення у версії 4. Більше того, тут є і вбудований тип даних UUID.

Oracle містить функцію sys_guid(). Версія UUID не вказується в документації, визначається лише унікальність поверненого значення і його формат — MAC-адреса, ідентифікатор потоку/процесу і значення автоінкрементного лічильника. Тут також є тип даних UUID.

Підтримка в ОС

У Windows використовується термін GUID замість UUID, хоча це фактично те саме. В описі GUID немає жодного слова про те, як він генерується, лише його формат. А у Win32 API для розробників є функції UuidCreate та UuidCreateSequential. Різниця між ними в тому, що перша не використовує MAC-адресу для генерації значень, а друга використовує. Windows широко використовує GUID для ідентифікації різних інтерфейсів, ACL і ActiveX об’єктів. Більше того, ви можете згенерувати UUID з командного рядка:

powershell -Command «[guid]::NewGuid().ToString()»

У Linux є утиліта uuidgen, яка використовує бібліотеку libuuid та підтримує генерацію всіх версій UUID (крім другої). Якщо у вас немає цієї утиліти, ви можете просто читати з read-only файлу /proc/sys/kernel/random/uuid, який щоразу повертатиме новий UUID.

Підтримка в Java

У Java 5 з’явився клас UUID, що дозволяє генерувати UUID версії 4:

UUID id = UUID.randomUUID();
System.out.println(id.variant()); // 2
System.out.println(id.version()); //4

Він використовує стандартний клас SecureRandom для генерації випадкових чисел, а той своєю чергою може застосувати три алгоритми для цього завдання:

  1. SHA1PRNG
  2. WINDOWS-PRNG
  3. DRBG

Спочатку в JDK використовувався генератор SHA1PRNG (SHA-1 pseudo-random number generator), але зараз його слід застосовувати тільки для зворотної сумісності. А за замовчуванням задіяно DRBG (Deterministic Random Bit Generator), який вже використовує SHA-256 хешування.

Мало хто знає, що в UUID є підтримка і версії 3:

UUID id = UUID.nameUUIDFromBytes(bytes);

Тут ви повинні самостійно об’єднати Namespace і Name і потім конвертувати їх у байтовий масив.

Втім, якщо вам не підходять UUID версій 3/4, ви можете самі згенерувати його значення будь-яким іншим способом і передати йому через конструктор. Потім можна перевести його у рядковий формат:

System.out.println(id); // 164d5b77-51ad-4290-a42f-0a0461dae078

Але при цьому, якщо ви зберігаєте UUID як послідовність символів, ви на це витрачаєте 36 байт. Можна перетворити отримане значення, використовуючи Base64-кодування (де зберігається 6 біт на символ), і тоді у вас вже буде всього 22 символи:

var encoder = Base64.getUrlEncoder().withoutPadding();
ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
buffer.putLong(id.getMostSignificantBits());
buffer.putLong(id.getLeastSignificantBits());
String base64Id = encoder.encodeToString(buffer.array()); // Fk1bd1GtQpCkLwoEYdrgeA

Якщо UUID найчастіше використовуються для баз даних, то невипадково, що й у ORM-технологіях роблять підтримку UUID-ідентифікаторів дедалі зручнішою. Починаючи з Hibernate 6 ви можете однією анотацією @UuidGenerator вказати Hibernate, щоб він використовував версію 4 для генерації UUID:

@Id
@GeneratedValue
@UuidGenerator
private UUID id;

Якщо ви хочете використовувати версію 1, то для цього потрібно явно в анотації @UuidGenerator вказати атрибут style:

@Id
@GeneratedValue
@UuidGenerator(style = Style.TIME)
private UUID id;

При цьому замість MAC-адреси буде братися IP-адреса сервера. Тут, правда, можуть виникнути колізії, якщо у вас сервери знаходяться в різних мережах, але мають один і той же IP.

Бібліотека Jackson з перших версій підтримує серіалізацію/десеріалізацію UUID-значень:

String json = "\"a7161c6c-be14-4ae3-a3c4-f27c2b2c6ef4\«";
ObjectMapper mapper = new ObjectMapper();
UUID uuid = mapper.readValue(json, UUID.class);

Підтримка JavaScript

Довгий час JavaScript не мав нативної підтримки UUID. У 2011-му році з’явилася бібліотека uuid, яка підтримувала всі версії UUID, крім другої. І тільки в 2021-му році до стандарту EcmaScript 2021 (ES12) додали функцію randomUUID(), яка генерує UUID версії 4:

const id = crypto.randomUUID(); // ffcf8cf8-2ec8-4493-a8c4-0d9e0a633eb7

Після цього підтримка UUID з’явилася в Node.js, починаючи з версії 14.17.:

import { randomUUID } from «node:crypto»;
const uuid = crypto.randomUUID();

У браузерах така підтримка вперше з’явилася у Chrome 92 та Firefox. При цьому з урахуванням вимог безпеки цю функцію можна використовувати тільки на HTTPS з’єднаннях та на HTTP (але на localhost).

Benchmarks

Будь-яке дослідження нової технології має включати аналіз її ефективності, тим більше, що ми маємо такий чудовий інструмент, як JMH. Для тестування було обрано наступну конфігурацію:

  • JMH 1.37
  • JDK 23.0.1
  • Intel Core i9, 8 cores
  • 32 GB
  • 10 ітерацій обчислення, 10 ітерацій прогріву (warm-up)

Для створення UUID я вибрав популярну бібліотеку java-uuid-generator, яка підтримує всі версії UUID, крім застарілих (2 і 3). Ось як виглядають тести:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
public class UuidBenchmark {

private final Map<Integer, UuidGenerator> generators = new HashMap<>();

@Param({ «1», «4», «5», «6», «7» })
private int version;

@Setup
public void setup() {
      generators.put(1, () -> Generators.timeBasedGenerator().generate());
      generators.put(4, () -> Generators.randomBasedGenerator().generate());
      generators.put(5, () -> Generators.nameBasedGenerator().generate("www.uuid.com"));
      generators.put(6, () -> Generators.timeBasedReorderedGenerator().generate());
      generators.put(7, () -> Generators.timeBasedEpochGenerator().generate());
}

@Benchmark
public UUID generateUuid() {
     return generators.get(version).generate();
}

interface UuidGenerator {
     UUID generate();
}

Результати вимірювань (середній час виконання операції у наносекундах):

Benchmark (version) Mode Cnt Score Error Units
UuidBenchmark.generateUuid 1 avgt 5 714.357 1.600 ns/op
UuidBenchmark.generateUuid 4 avgt 5 691.462 1.364 ns/op
UuidBenchmark.generateUuid 5 avgt 5 215.452 2.885 ns/op
UuidBenchmark.generateUuid 6 avgt 5 727.468 1.083 ns/op
UuidBenchmark.generateUuid 7 avgt 5 687.517 0.951 ns/op

Висновки

UUID — це набір версій/алгоритмів генерації унікальних значень, об’єднаних загальним форматом та структурою даних. Кожна з них має свої плюси та мінуси, виходячи з вимог до безпеки та сфери застосування. Це не стосується версій 2 і 3, які на цю мить застаріли і практично не застосовуються.

У чистій Java Core ви можете використовувати тільки версії 3 (застарілу) і 4 без застосування додаткових бібліотек. Бази даних підтримують тільки якусь одну версію, але самі версії відрізняються від СУБД до СУБД, що може ускладнювати міграцію з однієї бази даних на іншу.

Щодо швидкодії, то версії 1, 4, 6, 7 показали приблизно однаковий час. Ну а однозначно найкращий результат — у версії 5, що може бути пояснено відсутністю генерації випадкових чисел та обчислення поточного часу. І навіть застосування SHA-1 хешування не вплинуло на результат.

Загалом за унікальність потрібно платити свою ціну. Якщо при виборі цілого типу для первинного ключа ви використовуєте 4 (або 8) байт для кожного значення, то з UUID це вже буде 16 байт (або 32 символи). І це впливає не тільки на обсяг пам’яті, що витрачається, але і на пошук у базі даних по UUID, генерацію індексів і так далі.

👍ПодобаєтьсяСподобалось33
До обраногоВ обраному11
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Дещо некоректні деталі в історії.

> На сьогодні існує 8 версій формату UUID, при чому версія вказується в самому значенні й під неї відводиться 4 біти.

Це тільки в варіанті 2. А ще формально є інші варіанти. Від раннього DCOM залишилось, я чув, кілька UUID в використанні.

> після чого в 2005-му році з’явилася специфікація UUID від організації IETF (Internet Engineering Task Force), яку зараз всі й використовують, але вже зі 128-бітним значенням як ідентифікатор.

128-бітна версія була ще в DCE, на 1990 вже була. 64 біта це ще раніше. Специфікація від IETF тільки дала публічний вільно доступний стандарт (це суттєво, але не джерело!) існуючим форматам і правилам їх створення і використання. Всі 1-5 існували і широко використовувались до неї.

Про версію 1:

> Крім того, через широке застосування контейнерів і віртуальних мереж MAC-адреса могла бути неунікальною, що призводило до можливих колізій.

Щось слабо віриться, при генерації локального випадкового MAC складно зробити однакові. Хіба що ну дуже тупий генератор чи вибір фиксованих MAC зі значеннями типу «хост 1»...

Інше:

> Результати тестів показують, що використання UUID разом з ObjectId уповільнює продуктивність при вставці в 2-3 рази.

Це «разом». А замість — що буде?

> У Windows використовується термін GUID замість UUID, хоча це фактично те саме.

Різниця у серіалізації в бітовий рядок. Для Windows GUID воно стандартизоване у mixed endian (є поля big і є little). Для UUID стандартизовано big-endian. Хто його як зберігає — треба бути уважним.
Це ж треба тримати на увазі при отриманні з base64 формату.

Дуже цікаво написано. Дякую, Сергію!

Дякую за цікаву статтю.
Трохи оффтоп, але таки спитаю: є така річ як PBKDF2 нею роблять «сіль» для шифрування, якщо не стоїть питання навантаження (ідентифікатор потрібен 2-3 рази на хвилину) нею можна зробити унікальний як UUID й точно не прив’язаний до користувача ідентифікатор на 128 бітів тобто на рівні розміру хоч і не структури сумісний з UUID?

Якщо цей PBKDF2 використовує всередині генератор випадкових чисел, то гадаю, що і на виході у нього буде унікальне число.
Питання лише в тому, наскільки унікальне, тобто якийсь шанс колізії.

Жирний дізлайк. Чому не розібрано БД індекси на основі UUID — а саме сортування, фільтрування. UUID має дуже великі недоліки як ПК в Btree, тому і зявилася 7 версія.

Тут справа не в тому, як вам, програмісту, зручніше робити SELECT. Питання, як база даних зберігає індекси. Створені послідовно, UUIDv4 виглядатимуть випадковими. Відповідно, при записі значень цих індексів у базу даних, навіть якщо значення були створені в той самий проміжок часу, кластеризація навантажуватиме індекси при записі.

Уявіть, у вас є високонавантажена система. 100 серверів генерують нові записи з UUID кілька разів на секунду, і все це летить у Redis, який вантажить ці дані у Postgresql.

Ага. Ось тут життя з UUIDv7 стає простіше. Значення індексів не настільки розкидані і стежити за ними набагато простіше.

Плюс значення ключів можна дуже просто і швидко відсортувати в бінарному форматі. Візьміть перші 64 біти ідентифікатора і порівняйте їх як int64 з іншим ідентифікатором, і ви вже знаєте, який був створений раніше.

Напиши свою статтю ;)

Чудова стаття, дуже дякую

Єдине зауваження до редакції, чому текст на одруківки не вичитуєте?

Але не просто хешування, а хеширування

Хочу зазначити що НАВІТЬ якщо у вас 1 сервер БД, всеодно дуже варто використовувати саме UUID а не якийсь автоінкремент кей, для сек’юрності!!
Сек’юріті аудити завжди на це вказують!
Тож варто зробити це одразу щоб не переробляти потім.

Якщо автоінкрементний ідентифікатор, можна зрозуміти, які ідентифікатори були до поточного і використовувати для зловмисних запитів

Ось цей допис на форумі має PK = 52305, я подивився попередні дописи — наче це інкремент. Що я можу з цим зробити?

Можна піти читати попередні 52304 топіки :)

А хто ви думаєте зробив ті 10 млн переглядів у січні? ;)

На доу — нічого. А от якщо у вас є /users/123 то нормальна безпека видасть вам піздюлєй, бо це значить шо є ваше апі можно дрочить перебором інтів.

Ну ось я зіскрапив ДОУ і отримав 760 000 ідентифікаторів користувачів. Так, це складніше, ніж інкрементувати ідентифікатор, але реально. Що далі мені з цим робити?

«я знайшов портфеля з документацією як робити бомби. І шо я з нею можу зробити? нічого не можу. значить секретити такі дані немає сенсу» (ад абсурдум)
викинутий назовні передбачуваний ідентифікатор додає зручності (або й узагалі робить можливими) атаки, повз’язані зі знанням ідентифікатора.

ти не кулхацкєр (мабуть) шоб із цим шось робити, бо ти не вмієш і не знаєш що з цим робити.

Для людини компетентної у питаннях проникнення і зламу це додатковий який +1 до витягування інформації.

Ти ще не забувай, що доу — публічна штука і все що ти можеш зробити з ідентифікаторами і так публічне. Магазин, банк і тд — це вже сильно зовсім інша тема, бо наприклад,

shop/users/123/orders/12
говорить про те що конкретний юзер вже зробив як мінімум 12 ордерів, що по суті вже є витоком персональних даних.

Так, я не спеціаліст з безпеки. Тому й питаю.

Ось зробив я GET shop/users/123/orders/12 та отримав 403 Forbidden. Як з цієї інформації я можу дізнатись скільки замовлень зробив покупець?

1. не у всіх є валідація, то ж може видати і якийсь результат
2. конкурентам буде цікаво скільки ж юзерів і замовлень роблять у когось іншого

1. Є вірогідність що ід 12 це 12й ордер клієнта.
2. Велика вірогідність шо зробивши
GET shop/users/123/orders/123666565446
Ти отримаєш 404 NOT FOUND а не 403 FORBIDDEN чи 401 UNAUTHORIZED, таким чином перебором інтів 0-12 ти зможеш визначити всі ідентифікатори ордерів клієнта.

і тд і тп

Проблема з безпекою рідко проблема одного фактора

Автоінкрементні айдішніки дають тобі інформацію яка сама по собі може не важлива, але дає інформацію, в купі з іншими дірами призведе до витоку даних чи зламу

От ти дізнався сьогодні айдішнік, завтра дізнався кому він належить, післязавтра дізнався що у сайта новий сісадмін в якого буде один з наступних айдішніків, поставив наприклад скріпт який періодично прозвонює ендпойнти, чи перехопив якусь сесію з куками і швидко скриптом перевірив ендпойнт серією айдішніків

Ось Розетка, наприклад, має публічно видимий номер замовлення.
Вже можна розібрати темп замовлень у всьому порталі.
Далі, наприклад, порівняти темп замовлень у звичайний період, перед чорною пʼятницею і після Нового року. З цього — вже ефективність самої Ч.П. Ці дані можна порівняти за різні роки. І так далі.
Тут можна продовжувати цілими сторінками.
OSINT — да, це ціла наука. І не обовʼязково це про розвідки країн: комерційна розвідка не менше, а часто і більше впливова.

говорить про те що конкретний юзер вже зробив як мінімум 12 ордерів

Скоріш що всього в системі є як мінімум 12 ордерів, а не в цього юзера. Бо зазвичай це просто pk в таблиці orders і він «сквозной», незалежно від юзерів.

Якщо це сквозний пк, це ще гірше, ти зможеш проітерувать
GET shop/users/%USERID%/orders/%ORDERID% і скласти мапу у якого юзера є який ордер, фактично частково витягнувши інфу.

Не заперечую що можна перебрати все, просто кажу як частіше буває. Ось в rozetka, десь так і є.

Якщо у вас є URI в якому є автоінкрементні ідентифікатори, то вам буде дуже легко отримати дійсні URI для інших ідентифікаторів (наприклад, з id іншого користувача). Якщо ж в якості ідентифікаторів використовуються UUID, підібрати інший дійсний ідентифікатор набагато складніше.

Це так, але якщо у вас ідентифікатор у таблиці/базі даних є частиною публічного API, тобто є частиною URI.
Це перший момент, а другий момент, що можна використовувати не тільки UUID, є інші алгоритми генерації унікальних даних.

Це зовсім інша проблема і існує багато шляхів для її вирішення. Наприклад sqids.org краще захистить від перебору ніж uuid.

це вже зовсім інше рішення.
А чому використання такого плану шифраторів «краще захистить від перебору ніж uuid»?

Ніж uuid4 — ні. Ніж послідовні версії — да.
Але у всього є ціна.

в коменті Олега саме «ніж uuid», тому я й задав питання.

Якщо я буду генерувати uuid{неважливо якої версії}, ваш додаток буде витрачати ресурси на його обробку (лукап у базу ...). При використанні sqids є можливість перевірити його валідність на етапі декодування і тим самим одразу припинити його обробку.

ну це вийде оптимізація роботи, але не захист від перебору. бо ці sqids,hashids можна перебирати точно так, як і uuid. тим більш тут інформація не рандомна і її можна декодувати, або сформувати хеш з потрібними айдішниками.

Ймовірність того, що хтось вгадає ваш UUID/HashID, мінімальна. Я не стверджую, що кардинальність UUID нижча, ніж у HashID (хоча це залежить від налаштованої довжини хеша). Дискусія була про те, що лише UUID захистить від перебору, моя мета показати, що це не так. Щодо оптимізації обробки запиту, хіба вона не важлива для безпеки вашої системи? Крім того, HashID дозволяє виконати ротацію публічних ідентифікаторів у разі компрометації.

все так. Я, власне, причепився тільки до формулювання «краще захиситить від перебору», бо загалом нічого не маю проти подібних методик — сам використовую автоінкременти як primary key і hashIds коли треба викинути їх у потенційно небезпечне середовище.

Я перегруповував звичайний Uuid1 для забезпечення послідовності
$uuid = explode(’-’, $uuid);
$uuid = $uuid[2] . $uuid[1] . $uuid[0] . $uuid[3] . $uuid[4];

Дякую за цікаву статтю та пояснення фундаментальних визначень!

Монотонність (кожне наступне значення має бути за попереднім при лексикографічному сортуванні)

Слово «лексикографічному» тут зайве, тому що має відношення до сортування векторів (слів) заданих на певній множині (алфавіті). UUID сортуються як звичайні байти у памʼяті [RFC 9562 / 6.11. Sorting]:

UUIDv6 and UUIDv7 are designed so that implementations that require sorting (e.g., database indexes) sort as opaque raw bytes without the need for parsing or introspection.

Більш точний переклад «Monotonicity (each subsequent value being greater than the last)» — це «монотонність (кожне наступне значення більше за попереднє)».

Дякую за уточнення термінології

Дуже цікава стаття, дякую за корисну інформацію!

Дякую вам за фідбек

Підписатись на коментарі