Aviso de seguridad. SSRF en las herramientas de agente de SiYuan mediante DNS-Rebinding TOCTOU (Bypass de CheckHostSSRF)

CampoValor
Divulgado porjoysinleung (joysinleung@gmail.com)
Fecha del informe2026-08-13
ProductoSiYuan (思源笔记) — siyuan-note/siyuan
Módulo Gogithub.com/siyuan-note/siyuan/kernel
Versiones afectadas<= 3.8.0 (última versión publicada en el momento del informe; verificado dinámicamente en v3.8.0)
Versiones corregidas3.8.1
Componentekernel/util/httprequest.go (CheckHostSSRF), kernel/mcp/tools/http_request.go, kernel/util/webfetch.go, kernel/util/net.go (SSRFSafeDialer)
Relación con aviso anteriorVariante de corrección incompleta de GHSA-rg26-cg95-gq6p (corrección del vector principal de SSRF). Véase §Relación.
EPSS (probabilidad de explotación)Baja–Moderada. Requiere que el atacante influya en un Agente de IA / cliente MCP para que acceda a un dominio controlado por el atacante (escenario de inyección de prompt documentado por la propia herramienta).
KEV (CISA Known Exploited)No (no figuraba en CISA KEV en el momento del informe).
Alcanzable con configuración predeterminadaSí — explotable tanto con SafeMode activado como desactivado; solo requiere que la herramienta del agente http_request / web_fetch sea accesible (herramientas de IA predeterminadas).

Resumen

Las herramientas de Agente de IA de SiYuan http_request (util.HTTPRequest) y web_fetch (util.WebFetch) son la única barrera SSRF para las solicitudes salientes desde el kernel. Esa barrera es CheckHostSSRF, que realiza una única resolución DNS en el momento de la comprobación y verifica si alguna IP devuelta es privada/loopback/link-local. La conexión real, no obstante, realiza una segunda resolución DNS independiente mediante el net.Dialer predeterminado, y en esta ruta no se aplica ninguna comprobación de IP privada en el momento de la conexión.

Como las dos resoluciones no están fijadas al mismo resultado, un dominio controlado por un atacante puede responder a la resolución de comprobación con una IP pública (superando CheckHostSSRF) y a la resolución de conexión con una IP privada/loopback/de metadatos (por ejemplo, 169.254.169.254). Esto es un clásico DNS-rebinding TOCTOU que elude por completo la defensa SSRF. Permite alcanzar los metadatos de instancias en la nube y servicios internos que la protección se añadió específicamente para bloquear.

Relación con avisos anteriores

  • GHSA-rg26-cg95-gq6p corrigió el vector principal de SSRF añadiendo CheckHostSSRF (en tiempo de análisis) y SSRFSafeDialer (en tiempo de conexión). No obstante, las rutas de herramientas del agente (http_request / web_fetch) solo recibieron la mitad correspondiente al análisis: llaman a CheckHostSSRF pero después se conectan mediante httpclient.NewBrowserRequest(), cuyo transporte no monta SSRFSafeDialer. Incluso allí donde SSRFSafeDialer sí está montado, solo bloquea IP privadas cuando SafeMode == true (por defecto false), por lo que tampoco ayudaría aquí. La ruta hermana openai.go:generatedImageDialer sí monta un hook Control en tiempo de conexión (bloqueando private/loopback/link-local/100.64/198.18), lo que demuestra que el proyecto conoce la técnica; la ruta del agente es una omisión clara. Informamos de esto como una variante de corrección incompleta con una reproducción concreta en v3.8.0.
  • CVE-2026-32110 (GHSA-56cv-c5p2-j2wg) cubría el endpoint antiguo forwardProxy y no está relacionado con la ruta de herramientas del agente.

Versión afectada

Verificado dinámicamente en v3.8.0 (tag v3.8.0, commit 251596fc0). Se instaló un secuestro DNS a nivel de proceso para que el dominio del atacante rebind.local devolviera una IP pública (203.0.113.1) en las resoluciones impares (comprobación) y 127.0.0.1 en las resoluciones pares (conexión); un servicio "víctima" en loopback devolvió F9_REBIND_PROOF=reached-internal-only-service-via-TOCTOU. Tanto util.HTTPRequest("GET", "http://rebind.local:<port>/secret") como util.WebFetch(...) devolvieron el cuerpo de prueba del servicio interno, mientras que CheckHostSSRF("127.0.0.1") bloqueó directamente y el dominio público legítimo pub.local pasó la comprobación, confirmando que la barrera permitió la solicitud pero la conexión alcanzó la dirección interna. Todas las versiones <= 3.8.0 están afectadas.

Componente

  • kernel/util/httprequest.go:42 CheckHostSSRF — un único net.LookupIP + isPrivateIP únicamente en el momento de la comprobación.
  • kernel/mcp/tools/http_request.go:90 → util.HTTPRequest; kernel/util/webfetch.go:50 → CheckHostSSRF + httpclient.NewBrowserRequest().
  • github.com/siyuan-note/httpclient client.go:92 NewBrowserRequest utiliza el http.Transport predeterminado → net.Dialer predeterminado sin SSRFSafeDialer.
  • kernel/util/net.go:151 SSRFSafeDialer existe pero (a) no está montado en la ruta del agente y (b) solo está activo bajo SafeMode.
  • Ruta hermana correctamente protegida: kernel/util/openai.go:829 generatedImageDialer monta un hook Control en tiempo de conexión.

Vector de ataque

Red + Agente de IA. La url de http_request / web_fetch está completamente controlada por el agente / cliente MCP. En el escenario de red-team documentado por SiYuan ("inyectar un prompt al agente → inducirlo a visitar un dominio del atacante"), el atacante solo necesita un dominio de rebinding (DNS autoritativo propio, primera respuesta pública, después 169.254.169.254 / interna). Sin autenticación, sin posición especial más allá de inducir al agente mediante un prompt. Objetivos reales: metadatos de nube 169.254.169.254 (credenciales IAM temporales) y servicios no autenticados del mismo host/internos.

Prueba de concepto