
Aviso para CVE-2026-51385. Era necesario publicarlo ya que GRAPHIFY no ha reconocido el aviso ni lo ha publicado, y MITRE asignó CVE-2026-51385, este es el aviso para ello.
Aviso para CVE-2026-51385. Necesario publicarlo ya que GRAPHIFY no ha reconocido el aviso ni lo ha publicado, y MITRE asignó CVE-2026-51385, este es el aviso correspondiente.
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: Este fue mi primer CVE, para ser honesto, encontré la manera de romper la lógica en la función anti-SSRF, y realmente quería publicar el CVE, así que pensé cómo podría afectar a la seguridad, encontré una justificación, lo envié a MITRE y lo aceptaron. Este es el aviso.
Ten en cuenta que el informe fue elaborado parcialmente con IA, pero supervisado por un humano
SSRF mediante reenlace DNS (TOCTOU) en graphify.
Paquete: graphify (PyPI: graphifyy), repo Graphify-Labs/graphify
Componente afectado: la ruta de ingesta de URL, graphify add <url>
Versiones afectadas:
(commit , PRs #591 / #592)
8.3 (Alto),
(SSRF), (carrera TOCTOU)
Arturo Melgarejo Galindo, investigador de seguridad independiente ()
>=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> intenta protegerse de SSRF de la misma manera que muchos recolectores: resuelve el nombre de host, verifica la IP resultante contra una lista de bloqueo (loopback, RFC1918, link-local, reservadas) y solo recupera si esa verificación pasa.
El problema es que la IP que validó no es la IP a la que se conecta. La validación realiza una búsqueda DNS, y luego requests.get(hostname) realiza una segunda búsqueda independiente. Si eres dueño de la zona DNS del nombre de host y entregas un TTL muy bajo con dos registros A, uno público y otro interno, el resolvedor puede dar legalmente una respuesta diferente en cada consulta. La primera respuesta pasa la lista de bloqueo. La segunda respuesta es donde realmente va el socket. Ese es todo el error. Es la clásica evasión por reenlace contra la lógica de "verificar la IP, luego conectar por nombre", y la solución es fijar la IP validada a la conexión, que es lo que hace 0.5.4.
Quiero ser honesto al respecto, porque por sí solo parece más débil de lo que es.
Si una persona se sienta y escribe una URL que ya es de confianza en graphify add, esto es casi inútil. Esa persona ya puede apuntar graphify a su propio 127.0.0.1 si quiere. No necesita un truco de reenlace para eso, y no hay atacante en esa situación.
Se convierte en una vulnerabilidad real en el momento en que graphify se ejecuta contra una URL que el operador no eligió. Y para esta herramienta, eso no es un caso excepcional, es la forma normal en que se usa. graphify construye gráficos de conocimiento que son consumidos por asistentes de IA, por lo que el flujo realista es algún proceso que llama a graphify add en tu nombre: un agente, un trabajo de CI, un script que recorre una lista de URLs de un README, o una URL que salió directamente de la salida de otro modelo. En todos esos casos, el atacante controla la cadena de entrada y el humano nunca la inspeccionó.
Ese es el caso que importa. Una vez que la entrada no es confiable y se gana la carrera de reenlace, la descarga cae en una dirección interna en lugar de la pública que validó. Concretamente puede alcanzar:
127.0.0.1 y cualquier cosa vinculada a loopback,169.254.169.254 (metadatos de instancia en la nube),100.64.0.0/10, que la lista de bloqueo no cubre en absoluto, por lo que ni siquiera necesita la carrera, un simple registro A que apunte a él es suficiente.Y alcanzar esas direcciones solo se convierte en daño por lo que vive allí. Muchos servicios internos y de desarrollo exponen endpoints GET que devuelven datos o cambian de estado con un simple GET. Así que una descarga de graphify que cae en uno de ellos, con la ruta y cadena de consulta incorrectas, no es solo una lectura. Si el destino interno es un scriptText de Jenkins, un depurador Flask/Django en --debug, un panel de administración o un endpoint de metadatos, ese GET malformado es el atacante actuando desde dentro del perímetro. graphify es el diputado que hace la solicitud por ellos.
No estoy afirmando que esto incluya un RCE. No lo hace, por sí mismo. Pero un SSRF completo que pueda apuntarse a loopback, IMDS, RFC1918 y CGN desde una ruta de ingestión automatizada es exactamente la primitiva sobre la que se construyen esos ataques, y ese es el punto de reportarlo.
Usé el harness público de Tavis Ormandy rbndr.us, que te da un nombre de host que cambia entre dos IPs en cada resolución. 7f000001.08080808.rbndr.us alterna entre 8.8.8.8 y 127.0.0.1.
Inicia algo en el destino interno (aquí, loopback, para la demostración):
sudo python3 -m http.server 80
Luego activa la ingestión y reintenta hasta que la carrera acierte. Acierta aproximadamente 1 de cada 4 o 5 intentos, y un bucle trivial lo lleva mucho más allá del 99%:
for i in {1..20}; do
graphify add http://7f000001.08080808.rbndr.us/ && break
sleep 1
done
En un intento exitoso, graphify ingiere lo que sea que el servidor local en 127.0.0.1:80 devuelva, aunque el nombre de host pasó primero la lista de bloqueo de IP.
0.5.4 (dd86271), 8 días después.Arturo Melgarejo Galindo, investigador de seguridad independiente.