Mi primer CVE: Open Redirect en un proyecto con +2 millones de descargas

Desde hace ya un tiempo comencé a hacer vulnerability research en proyectos open source en mi tiempo libre. Mi metodología es bastante sencilla: navego por GitHub buscando proyectos que me llaman la atención, clono el repositorio y me pongo a investigarlo, tanto a nivel de código como interactuando con él en local. De esta manera conseguí mi primer CVE, en Kiwi TCMS, un proyecto con más de 2 millones de descargas que busca mejorar el manejo de tests, tanto de forma manual como automática.

Resumen

Kiwi TCMS tenía un Open Redirect (CWE-601) en el endpoint de confirmación de cuenta, accesible sin necesidad de autenticación ni de una clave de activación válida. El parámetro next de la URL se usaba para construir la redirección final sin validar que apuntara al propio dominio, por lo que cualquiera podía construir un enlace alojado en una instancia legítima de Kiwi TCMS que redirigiera a la víctima a un dominio externo arbitrario. El hallazgo se registró como CVE-2026-54724.

CVE: CVE-2026-54724  ·  CVSS: 6.1 (Moderate)  ·  Producto: Kiwi TCMS ≤ 16.0

Entorno y versiones afectadas

Versiones afectadas: ≤ 16.0  ·  Versión corregida: 16.1

Análisis de la vulnerabilidad

La vulnerabilidad es un Open Redirect en el endpoint /accounts/confirm/<activation_key>/, a través del query param ?next=<url>. Este endpoint se usa para confirmar el correo de un usuario nuevo, o de uno ya existente que ha cambiado de correo.

Encontrarla no fue muy complicado, tampoco es una lógica demasiado enrevesada. Levanté el proyecto en local y, probando de forma dinámica, me di cuenta de que el endpoint siempre redirigía a la URL indicada en next, incluso pasando una clave de activación arbitraria.

Una vez entendido el comportamiento, revisé el código fuente para localizar el error. En la vista Confirm, en tcms/kiwi_auth/views.py, se asigna self.url al parámetro next sin ningún tipo de validación del host:

class Confirm(RedirectView):
    def get_redirect_url(self, *args, **kwargs):
        self.url = self.request.GET.get("next", reverse("core-views-index"))  # no validation
        activation_key = kwargs["activation_key"]
        try:
            _activation_key = UserActivationKey.objects.select_related("user").get(
                activation_key=activation_key
            )
        except UserActivationKey.DoesNotExist:
            messages.add_message(...)
            return super().get_redirect_url(*args, **kwargs)  # redirects to self.url
        ...
        return super().get_redirect_url(*args, **kwargs)      # also redirects to self.url

self.url se asigna antes de comprobar la clave de activación, así que cualquier clave de activación, junto con cualquier URL en este parámetro, hace que el endpoint devuelva un 302 redirigiendo a esa URL.

Prueba de concepto (PoC)

No es necesaria ni cuenta, ni sesión, ni clave de activación válida para explotar esta vulnerabilidad. El PoC se reduce a una simple request:

GET /accounts/confirm/any-string-here/?next=https://blog.martindios.com HTTP/1.1
Host: <kiwi-instance>
User-Agent: Mozilla/5.0
Accept: text/html

Cuya respuesta es:

HTTP/1.1 302 Found
Location: https://blog.martindios.com/

El impacto de esta vulnerabilidad es el típico de cualquier open redirect: phishing. Esta herramienta se suele desplegar como herramienta interna, por lo que los usuarios tienden a confiar más en sus enlaces. Un atacante podría usarla para redirigir a las víctimas a una página falsa de login y recolectar credenciales, para saltarse filtros de seguridad de correo (al provenir de un dominio de confianza), o para distribuir malware aprovechando el contexto de "confirm your account".

Mitigación

En la versión 16.1 de Kiwi TCMS se añadió una comprobación que obliga a redirigir a / si la URL del parámetro no empieza por /:

if not self.url.startswith("/"):
    self.url = reverse("core-views-index")

Conclusiones

Esta no es la vulnerabilidad más vistosa ni la de mayor impacto, pero estoy orgulloso de ella, ya que es mi primer CVE. Desde hace tiempo soy un entusiasta de software libre, y poder contribuir, aunque sea un poco, a mejorar su seguridad es muy gratificante. Seguiré revisando proyectos open source en mi tiempo libre, así que seguramente este no sea el último post de este estilo por aquí.

Referencias