Защита от DoS через схемы

Валидация входных данных в JavaScript через JSON Schema и библиотеку Ajv часто воспринимается как чисто прикладная задача: проверить структуру объекта и пропустить его дальше по пайплайну. Однако сама схема валидации может становиться источником уязвимостей класса DoS (Denial of Service), когда злоумышленник передаёт специально подобранные данные, приводящие к чрезмерному потреблению CPU или памяти.

В случае Ajv такие проблемы обычно связаны не с самой библиотекой, а с тем, как построены схемы: сложные регулярные выражения, глубоко вложенные структуры, неограниченные массивы, избыточные логические комбинации и рекурсивные ссылки.

Категории DoS-рисков при использовании JSON Schema

Регулярные выражения и ReDoS

Одним из наиболее опасных источников DoS является использование pattern в схемах JSON Schema. Ajv компилирует регулярные выражения в JavaScript RegExp, а значит подвержен классическим проблемам ReDoS (Regular Expression Denial of Service).

Опасность возникает, когда выражение допускает катастрофическое бэктрекинг-поведение:

  • вложенные квантификаторы (a+)+
  • неоднозначные альтернативы (a|aa)+
  • отсутствие якорей начала/конца строки

При обработке длинных строк время проверки может расти экспоненциально.

Пример проблемного паттерна:

{
  "type": "string",
  "pattern": "^(a+)+$"
}

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

Глубокая вложенность объектов

JSON Schema позволяет описывать рекурсивные структуры через $ref, allOf, anyOf, oneOf. Неправильно спроектированные схемы могут приводить к:

  • глубокой рекурсии
  • повторной проверке одних и тех же ветвей
  • росту сложности в геометрической прогрессии

Особенно опасны конструкции:

  • allOf с пересекающимися условиями
  • цепочки $ref без ограничения глубины
  • комбинации anyOf + oneOf на больших структурах

Пример проблемной схемы:

{
  "allOf": [
    { "$ref": "#/definitions/node" },
    { "$ref": "#/definitions/node" }
  ]
}

При неудачной архитектуре это приводит к многократной повторной валидации одних и тех же узлов.

Неограниченные массивы и объекты

Отсутствие ограничений maxItems, maxProperties и maxLength создаёт возможность подачи огромных структур данных.

Проблемные случаи:

  • массивы с миллионами элементов
  • объекты с тысячами свойств
  • строки неопределённой длины

Даже при линейной сложности алгоритма это приводит к перегрузке CPU и памяти.

Специфика Ajv и потенциальные точки нагрузки

Библиотека Ajv оптимизирована для скорости, но её производительность сильно зависит от схемы. При компиляции схемы в функцию валидатора происходит генерация JavaScript-кода, и ошибки проектирования схемы напрямую влияют на runtime.

Компиляция схем

Ajv компилирует каждую схему:

const validate = ajv.compile(schema);

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

Рекомендуется:

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

allOf / anyOf / oneOf как мультипликатор сложности

Комбинаторные операторы увеличивают количество проверок:

  • allOf требует прохождения всех схем
  • anyOf проверяет до первого успеха, но может проверять все варианты
  • oneOf требует проверки всех вариантов для исключения множественных совпадений

Пример:

{
  "oneOf": [
    { "$ref": "#/defs/a" },
    { "$ref": "#/defs/b" },
    { "$ref": "#/defs/c" }
  ]
}

При вложенных oneOf сложность может расти экспоненциально.

Защита через ограничение структуры данных

Ограничение глубины

JSON Schema не имеет универсального встроенного ограничения глубины, поэтому его нужно задавать архитектурно:

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

Пример защитного подхода:

{
  "type": "object",
  "maxProperties": 20,
  "properties": {
    "children": {
      "type": "array",
      "maxItems": 10,
      "items": {
        "$ref": "#"
      }
    }
  }
}

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

Явные ограничения массивов и строк

Критически важно всегда задавать:

  • maxItems
  • minItems
  • maxLength
  • minLength

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

Регулярные выражения: безопасное проектирование

При использовании pattern следует избегать:

  • вложенных квантификаторов
  • неоднозначных группировок
  • чрезмерно длинных альтернатив

Безопасные практики:

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

Иногда более безопасным решением является замена regex на:

  • перечисления (enum)
  • кастомные проверки через format
  • отдельную бизнес-валидацию вне схемы

Строгий режим Ajv и предотвращение неопределённого поведения

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

Полезные опции:

  • strict: true
  • strictTypes: true
  • strictSchema: true

Они помогают обнаружить:

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

Строгий режим не предотвращает DoS напрямую, но снижает вероятность появления сложных и неконтролируемых схем.

Контроль ветвления схем

Особое внимание требуется при использовании:

  • if / then / else
  • вложенных anyOf внутри условий

Пример опасной конструкции:

{
  "if": {
    "properties": { "type": { "const": "A" } }
  },
  "then": { "$ref": "#/defs/a" },
  "else": {
    "if": {
      "properties": { "type": { "const": "B" } }
    },
    "then": { "$ref": "#/defs/b" }
  }
}

Такие конструкции могут привести к каскадной проверке множества ветвей.

Рекомендуется:

  • минимизировать вложенные условия
  • заменять сложную логику на явное поле-диспетчер (discriminator)
  • избегать глубоких цепочек if

Предварительная фильтрация входных данных

Даже при идеальной схеме важно ограничивать данные до попадания в Ajv:

  • ограничение размера payload на уровне HTTP
  • проверка длины JSON-строки до парсинга
  • ограничение глубины JSON (до десериализации)

Это предотвращает ситуацию, когда валидатор вообще не успевает начать работу.

Работа с $ref и циклическими зависимостями

Циклические ссылки сами по себе допустимы, но при сложной структуре они могут приводить к:

  • повторной проверке одних и тех же поддеревьев
  • увеличению времени валидации

Пример:

{
  "$ref": "#/definitions/node"
}

где node ссылается на самого себя через цепочку объектов.

Важно:

  • минимизировать количество уровней косвенности
  • избегать множественных перекрёстных ссылок

Практическая модель защиты

Защита от DoS в контексте Ajv строится не на одном механизме, а на комбинации:

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

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