LDAP и NoSQL: тот же принцип, другой синтаксис

Инъекции не заканчиваются на SQL: любой интерпретатор запросов уязвим к тому же приёму.

NoSQL-инъекция — внедрение управляющих структур в запрос документной/key-value БД; LDAP-инъекция — внедрение спецсимволов в фильтр LDAP. Механизм тот же: данные становятся частью запроса.

Почему «данные ≠ код» универсален

Сменив SQL на MongoDB или LDAP, мы не уходим от проблемы инъекций — мы лишь меняем синтаксис интерпретатора. Везде, где недоверенные данные собираются в запрос конкатенацией или подставляются в структуру без разделения, возможна инъекция. Поэтому защита та же: разделять данные и команду, использовать параметризацию/безопасные билдеры и валидировать ввод.

Эта мысль важна психологически, потому что вокруг «не-SQL» технологий витает ложное чувство безопасности. Раз нет SQL — значит, нет и SQL-инъекции, рассуждают по инерции, и забывают, что инъекция привязана не к SQL, а к самому факту наличия интерпретатора запросов. У документной БД свой язык условий, у каталога LDAP — свой синтаксис фильтров, у поисковика — свой DSL; каждый из них с радостью истолкует подсунутые данные как часть команды, если им это позволить. Отсутствие слова «SQL» в названии технологии не отменяет ни одного из законов этой главы.

Хорошая новость в том, что и решать ничего нового не нужно. Тот же принцип «данные ≠ код» переносится дословно: не склеивать запрос из строк, передавать значения отдельно от структуры, а где разделения нет — экранировать официальной функцией и ограничивать ввод по типу и allowlist'у. Дальше мы посмотрим, как этот общий рецепт выглядит на двух конкретных примерах — NoSQL и LDAP, — и убедимся, что меняется только синтаксис, а суть остаётся прежней.

NoSQL-инъекция: операторы вместо значений

В документных БД запрос — это объект. Если поле объекта берётся из ввода без проверки типа, пользователь может прислать вместо строки оператор, меняющий смысл условия.

// Уязвимо: значение приходит как объект-оператор
// ожидали:  { password: "1234" }
// прислали: { password: { "$ne": null } }   -> "пароль не равен null" -> всегда истина
db.users.find({ user: input.user, password: input.password });

// Безопасно: приводим к строке и/или валидируем тип
if (typeof input.password !== "string") reject();
db.users.find({ user: String(input.user), password: String(input.password) });

Ключевая защита — контроль типа: поле, которое должно быть строкой, обязано быть строкой, а не объектом с операторами $ne, $gt, $where. Многие фреймворки умеют запрещать операторы во вводе или санитизировать ключи с $.

Особенность NoSQL-инъекции в том, что «управляющим символом» здесь выступает не кавычка и не точка с запятой, а сама структура присланного значения. Уязвимость рождается из удобной, но коварной возможности современных API — принимать JSON-тело и сразу превращать его в объект запроса. Если поле password ожидалось строкой, а пришло объектом { "$ne": null }, и этот объект без проверки попал прямо в условие поиска, то пользователь фактически дописал в запрос оператор сравнения. Условие «пароль равен присланному» превратилось в «пароль не равен null», истинное почти всегда, — и проверка пароля обойдена.

Отсюда и форма защиты. Контроль типа — это применение принципа «данные ≠ код» к документным БД: значение, которое по смыслу должно быть простой строкой, нельзя пускать в запрос объектом, способным нести операторы. Привести ввод к ожидаемому типу, отвергнуть поля-ключи, начинающиеся с $, описать форму запроса схемой — всё это разные способы гарантировать, что пользователь поставляет только значения, а структуру условия задаёте вы. Многие ORM и middleware для документных БД предоставляют такую защиту из коробки; её стоит включать осознанно, а не надеяться, что «оно само».

LDAP-инъекция: спецсимволы фильтра

Фильтры LDAP используют синтаксис со скобками и операторами: (&(uid=alice)(active=TRUE)). Символы ( ) * \ & | управляют логикой. Ввод со * может превратить точный поиск в «любой».

// Уязвимо: ввод склеен в фильтр
filter = "(uid=" + username + ")";
// username = "*"  ->  (uid=*)  -> совпадёт с любым пользователем

// Безопасно: экранируем спецсимволы фильтра (RFC 4515) либо берём
// готовый безопасный билдер фильтров из LDAP-библиотеки
filter = "(uid=" + escapeLdapFilter(username) + ")";

Экранирование по правилам LDAP заменяет опасные символы их безопасными представлениями (например, *\2a), и они перестают управлять логикой фильтра.

LDAP интересен тем, что у него нет привычной по SQL «параметризации» — фильтр всегда собирается как строка. Поэтому здесь на первый план выходит второй пункт нашего общего рецепта: официальное экранирование спецсимволов фильтра по RFC 4515. Символы (, ), *, \ и нулевой байт в LDAP управляют структурой выражения; экранирование переводит их в безопасную шестнадцатеричную запись, после чего интерпретатор фильтра видит их как обычные данные, а не как управляющие конструкции. Самый показательный пример — звёздочка: в точном поиске (uid=alice) она безобидна, но во вводе * без экранирования превращает условие в (uid=*), совпадающее с кем угодно.

На практике писать это экранирование руками не нужно и даже вредно — правила тонкие, и легко пропустить символ или ошибиться с кодировкой. Зрелые LDAP-библиотеки дают либо готовую функцию экранирования, либо безопасный билдер фильтров, который сам разделяет структуру и подставляемые значения. Это ровно та же стратегия «возьми проверенный инструмент вместо самодельного», что и параметризация в SQL: ответственность за корректное обезвреживание переносится с вашей памяти на отлаженный код библиотеки.

Как работает под капотом: общий чеклист для нового интерпретатора

Встретив незнакомый язык запросов (GraphQL, XPath, шаблонизатор, поисковый DSL), задайте три вопроса:

1. Есть ли механизм параметров/привязки значений? -> используйте его.
2. Если нет — есть ли официальная функция экранирования? -> экранируйте.
3. Можно ли ограничить ввод типом и allowlist'ом? -> ограничьте.

Так общий принцип переносится на любую технологию без заучивания частных рецептов.

Этот чеклист ценен тем, что превращает безопасность из «знаю наизусть уязвимости каждой технологии» в «умею задать три правильных вопроса». Когда в проекте появляется незнакомый язык запросов, не нужно ждать, пока выйдет статья про инъекции именно в нём, — достаточно пройтись по пунктам и найти штатный безопасный путь. Почти всегда он уже существует: авторы интерпретаторов знают про инъекции и предусматривают параметры, функции экранирования или строгую типизацию. Задача разработчика — осознанно ими воспользоваться, а не изобретать собственную «чистку» ввода, которая почти наверняка окажется неполной.

Частые ошибки

  • Доверять JSON-телу по структуре. Поле-«строка» может прийти объектом-оператором — проверяйте тип.
  • Своё экранирование LDAP. Правила RFC 4515 тонкие; берите функцию из библиотеки.
  • Считать, что NoSQL «безопасен по природе». Отсутствие SQL не отменяет инъекции.

Итоги

  • Принцип «данные ≠ код» работает в LDAP, NoSQL и любом языке запросов.
  • NoSQL: контролируйте тип ввода, запрещайте операторы там, где ждёте строку.
  • LDAP: экранируйте спецсимволы фильтра или используйте безопасный билдер.
  • Для нового интерпретатора: параметры → экранирование → типы и allowlist.
Проверьте себя
1. Как возникает типичная NoSQL-инъекция в документной БД?
AЧерез переполнение буфера
BПоле, ожидаемое как строка, принимается как объект-оператор (например, {"$ne": null}), меняя условие запроса
CЧерез слабый пароль администратора БД
DИз-за отсутствия индексов
2. Какая защита наиболее уместна против NoSQL-инъекции операторами?
AШифрование диска
BКонтроль типа ввода: поле-строка должно быть строкой, а не объектом с операторами
CУвеличение тайм-аутов
DОтключение индексов
3. Почему собственное экранирование LDAP-фильтра — плохая идея?
ALDAP не поддерживает экранирование
BПравила экранирования (RFC 4515) тонкие, и ручная реализация легко пропускает спецсимволы; надёжнее библиотечная функция
CЭкранирование замедляет поиск
DLDAP не уязвим к инъекциям