DoS через сложные схемы

Библиотека Joi предназначена для декларативного описания схем данных и их строгой проверки. Однако сама природа схемной валидации создаёт класс уязвимостей, связанных не с логической ошибкой, а с вычислительной нагрузкой. При определённых конструкциях схем возможно организовать отказ в обслуживании (DoS) за счёт экспоненциального роста сложности валидации.

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


Экспоненциальная сложность через alternatives и вложенные ветвления

Одним из наиболее опасных механизмов является использование alternatives().try() и вложенных условий, создающих дерево вариантов.

Каждое дополнительное альтернативное правило увеличивает количество путей проверки:

Joi.alternatives().try(
  Joi.object({
    type: Joi.string().valid('a'),
    value: Joi.string()
  }),
  Joi.object({
    type: Joi.string().valid('b'),
    value: Joi.number()
  }),
  Joi.object({
    type: Joi.string().valid('c'),
    value: Joi.boolean()
  })
)

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


Комбинаторный взрыв в when() зависимостях

Механизм условной валидации через when() позволяет динамически менять схему в зависимости от значения других полей. При глубокой цепочке зависимостей возникает ситуация, когда одно поле влияет на другое, которое влияет на третье и так далее.

Пример структуры риска:

  • поле A определяет схему B
  • поле B определяет схему C
  • поле C возвращается к влиянию на A

Такие циклы создают необходимость повторной оценки условий и пересборки схемы на каждом этапе проверки. В результате валидатор может многократно проходить по одним и тем же данным.


Регулярные выражения как источник катастрофического backtracking

Особую опасность представляют Joi.string().pattern() при использовании сложных регулярных выражений.

Регулярные выражения с пересекающимися квантификаторами способны приводить к экспоненциальному backtracking:

Joi.string().pattern(/(a+)+b/)

При подаче строки вида "aaaaaaaaaaaaaaaaaaaaaaaaaab" количество шагов проверки резко возрастает. В контексте массовых запросов это становится полноценным вектором DoS, так как один запрос может занимать непропорционально большое количество CPU-времени.


Глубоко вложенные объекты и рекурсивные схемы

Валидация объектов с вложенностью через Joi.object().keys() или Joi.array().items() создаёт риск переполнения стека и значительного увеличения времени обработки.

Особенно опасны схемы вида:

  • массив объектов
  • внутри каждого объекта снова массив
  • внутри него альтернативы и условия

При отсутствии ограничений глубины структура данных может быть искусственно расширена до десятков тысяч узлов.


Деградация через массивы с items() и неоднородными схемами

Конструкция:

Joi.array().items(
  Joi.object({ ... }),
  Joi.string(),
  Joi.number(),
  Joi.alternatives().try(...)
)

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

Если внутри элементов присутствуют ещё и when(), фактическая стоимость проверки становится многократно выше линейной.


Дублирование вычислений в составных схемах

При отсутствии кэширования промежуточных результатов одна и та же под-схема может пересчитываться многократно в разных ветках проверки.

Типичный пример:

  • одна и та же структура используется в alternatives
  • повторно используется в array.items
  • снова применяется в when

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


Деградация через пользовательские валидаторы

Функции custom() открывают возможность внедрения произвольной логики. Даже непреднамеренно сложная логика внутри таких функций может привести к линейной или квадратичной деградации производительности.

Особенно опасны:

  • циклы по входным данным без ограничений
  • вложенные проверки существования элементов
  • обращение к внешним ресурсам внутри валидатора

Масштабирование атаки через батч-запросы

При массовой отправке запросов с различными вариациями входных данных эффект от сложных схем усиливается:

  • каждый запрос запускает полный цикл валидации
  • отсутствует общий кэш между запросами
  • CPU насыщается неравномерной нагрузкой

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


Практики снижения вычислительной нагрузки

Снижение риска DoS через сложные схемы достигается не усложнением защиты, а ограничением выразительности схем:

  • ограничение глубины вложенности объектов и массивов
  • отказ от циклических when() зависимостей
  • минимизация использования alternatives().try() в пользу явных discriminated union через valid() поля
  • упрощение регулярных выражений и исключение вложенных квантификаторов
  • предварительная компиляция схем через Joi.compile() для устранения повторных вычислений структуры
  • разделение сложной схемы на независимые валидаторы по этапам

Характерные паттерны уязвимых схем

На практике наиболее опасные схемы обладают следующими признаками:

  • множественные уровни alternatives
  • условные зависимости между 3+ полями
  • регулярные выражения с повторяющимися группами
  • массивы с неоднородными типами элементов
  • повторное использование одной и той же схемы в разных частях дерева
  • отсутствие ограничений глубины и размера входных данных

Такие конструкции не приводят к ошибкам исполнения, но создают предсказуемую деградацию производительности при специально сформированных входах.