
Консультация для CVE-2026-51385. Необходимо опубликовать, так как GRAPHIFY не распознал и не опубликовал консультацию, а MITRE присвоил CVE-2026-51385, это консультация для него.
Рекомендация по CVE-2026-51385. Необходимо было опубликовать её, поскольку GRAPHIFY не распознала рекомендацию и не опубликовала её, а MITRE присвоила CVE-2026-51385; это и есть рекомендация.
ES: Este fue mi primer cve, para ser honesto encontre una manera de romper la logica en la funcion ANTI SSRF, y realmente queria publicar mi primer CVE asi que pense como podria afectar a la seguridad para demotrar impacto, aunque fuera compleja de ejecutar (es complicado que esto ocurra en una instancia real, pero posible por eso tiene complexity high). Lo envie al mitre y lo aceptaron. Este es el advisory.
EN: Это был мой первый CVE, честно говоря, я сам нашел способ обмануть логику функции анти-SSRF, и мне очень хотелось опубликовать CVE, поэтому я подумал, как это может повлиять на безопасность, нашел обоснование, отправил в MITRE, и они приняли. Это и есть рекомендация.
Имейте в виду, что отчёт был частично составлен с помощью ИИ, но под контролем человека.
SSRF через DNS-ребinding (TOCTOU) в graphify.
Пакет: graphify (PyPI: graphifyy), репозиторий Graphify-Labs/graphify
Затронутый компонент: путь приёма URL, graphify add <url>
Затронутые версии:
(коммит , PRs #591 / #592)
8.3 (Высокий),
(SSRF), (TOCTOU race)
Arturo Melgarejo Galindo, независимый исследователь безопасности ()
>=0.3.2, <=0.4.290.5.4CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:Hgraphify add <url> пытается защититься от SSRF так же, как это делают многие загрузчики: он разрешает имя хоста, проверяет полученный IP на соответствие чёрному списку (loopback, RFC1918, link-local, зарезервированные) и загружает данные только в том случае, если проверка пройдена.
Проблема в том, что проверенный IP — это не тот IP, к которому он подключается. Проверка выполняет один DNS-запрос, а затем requests.get(hostname) выполняет второй, независимый. Если вы владеете DNS-зоной этого хоста и отдаёте очень низкий TTL с двумя A-записями — одной публичной и одной внутренней, — резолвер может легально дать разные ответы на каждый запрос. Первый ответ проходит чёрный список. Второй ответ определяет, куда на самом деле пойдёт сокет. В этом и заключается вся ошибка. Это классический обход через rebinding для логики «проверь IP, затем подключись по имени», и исправление заключается в привязке проверенного IP к соединению — именно это и делает версия 0.5.4.
Я хочу быть честным на этот счёт, потому что сама по себе она выглядит слабее, чем есть.
Если человек садится и вводит в graphify add уже знакомый ему URL, то эта атака практически бесполезна. Этот человек и сам может направить graphify на свой 127.0.0.1, если захочет. Ему не нужен трюк с rebinding, и в такой картине нет злоумышленника.
Уязвимость становится реальной, когда graphify обрабатывает URL, который оператор не выбирал. И для этого инструмента это не крайний случай, а обычный сценарий использования. graphify строит графы знаний, которые потребляются ИИ-ассистентами, поэтому реальный поток выглядит так: какой-то процесс вызывает graphify add от вашего имени — агент, CI-задача, скрипт, проходящий по списку URL из README, или URL, полученный прямо из выходных данных другой модели. Во всех этих случаях злоумышленник контролирует входную строку, а человек её не проверял.
Именно этот случай важен. Как только входные данные становятся недоверенными и гонка rebinding выиграна, запрос приземляется на внутренний адрес вместо публичного, который был проверен. Конкретно он может достичь:
127.0.0.1 и всего, что привязано к loopback,169.254.169.254 (метаданные облачных инстансов),100.64.0.0/10, который вообще не покрывается чёрным списком, так что для него даже не нужна гонка — достаточно обычной A-записи, указывающей внутрь этого диапазона.А ущерб от достижения этих адресов возникает из-за того, что там находится. Множество внутренних и dev-сервисов предоставляют GET-эндпоинты, которые либо возвращают данные, либо изменяют состояние при простом GET. Так что запрос graphify, приземлившийся на один из них с неправильным путём и строкой запроса, — это не просто чтение. Если внутренняя цель — это Jenkins scriptText, отладчик Flask/Django в режиме --debug, админ-панель или эндпоинт метаданных, то этот неверно сформированный GET становится действием злоумышленника изнутри периметра. graphify выступает в роли заместителя, который выполняет запрос за него.
Я не утверждаю, что это даёт RCE само по себе. Но полный SSRF, который можно направить на loopback, IMDS, RFC1918 и CGN из автоматизированного пути приёма, — это именно тот примитив, на котором строятся такие атаки, и именно ради этого стоит сообщать об уязвимости.
Я использовал публичную утилиту Tavis Ormandy rbndr.us, которая предоставляет имя хоста, переключающееся между двумя IP-адресами при каждом разрешении. 7f000001.08080808.rbndr.us чередует 8.8.8.8 и 127.0.0.1.
Запустите что-нибудь на внутреннем целевом хосте (здесь для демонстрации — loopback):
sudo python3 -m http.server 80
Затем запустите приём URL и повторяйте, пока гонка не сработает. Попадание происходит примерно в 1 из 4–5 попыток, а простой цикл доводит вероятность до значительно выше 99%:
for i in {1..20}; do
graphify add http://7f000001.08080808.rbndr.us/ && break
sleep 1
done
При успешной попытке graphify обрабатывает то, что вернул локальный сервер на 127.0.0.1:80, несмотря на то, что имя хоста сначала прошло проверку по чёрному списку IP.
0.5.4 (dd86271), 8 дней спустя.Arturo Melgarejo Galindo, независимый исследователь безопасности.