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.