Un open redirect es cualquier página de su sitio que envía el navegador del visitante a una URL tomada de la entrada del usuario sin validarla. Parece un fallo menor. En la práctica permite a un atacante enviar un enlace de phishing que muestra su dominio real y, en un entorno OAuth, puede filtrar códigos de autorización y access tokens.
Por qué una redirección es más que un fallo cosmético
CWE-601 describe la debilidad como una aplicación web que acepta una entrada controlada por el usuario que especifica un enlace a un sitio externo y después usa ese enlace en una redirección. La víctima sigue un enlace a su sitio de confianza y acaba en una página controlada por el atacante. La cheat sheet de OWASP ofrece este ejemplo:
http://example.com/example.php?url=http://malicious.example.com
El nombre de servidor del enlace modificado es idéntico al del sitio original, de modo que el intento de phishing resulta más creíble. Para la parte que afecta al usuario, consulte nuestro verificador de enlaces.
Con OAuth, el mismo fallo llega más lejos. RFC 9700 establece que los clientes y los servidores de autorización NO DEBEN (MUST NOT) exponer URLs que reenvíen el navegador a URIs arbitrarias tomadas de un parámetro de consulta, porque los open redirectors pueden facilitar la exfiltración de códigos de autorización y access tokens.
Los forwards son un caso relacionado. Si la entrada del usuario elige un destino interno, como en function.jsp?fwd=admin.jsp, una URL manipulada puede superar la comprobación de control de acceso y alcanzar una función administrativa. La aplicación debe comprobar que el usuario está autorizado para acceder al destino.
Localizar los parámetros
Los nombres varían: url, fwd y returnUrl aparecen en los ejemplos de OWASP, y su código tendrá otros. Busque el patrón, no el nombre.
| Stack | Patrón peligroso |
|---|---|
| Java | response.sendRedirect(request.getParameter("url")) |
| PHP | header("Location: " . $redirect_url) con $_GET['url'] |
| C# .NET | Response.Redirect(url) con request.QueryString["url"] |
| Rails | redirect_to params[:url] |
| Forward en Java | getRequestDispatcher(fwd).forward(...) |
Una primera pasada con grep le da una lista que revisar. Ajuste la ruta y los tipos de archivo a su repositorio.
grep -rnE 'sendRedirect\(|Redirect\(|redirect_to params|header::LOCATION' src/
grep -rn -A3 'header(.Location' src/
El segundo comando es para PHP. Lea las líneas posteriores a cada coincidencia: si no hay un exit después de header("Location..., el resto del script sigue ejecutándose. Un navegador configurado para ignorar la redirección podría entonces mostrar el resto de la página.
Los equipos .NET deberían empezar por las acciones de inicio de sesión. La cheat sheet indica que los sitios con ASP.NET MVC 1 y 2 son especialmente vulnerables, y su ejemplo de LogOn en MVC 2 redirige a returnUrl sin ninguna validación. Su solución es migrar a MVC 3.
La mayoría de las coincidencias serán inofensivas. Toda redirección cuyo argumento no sea un literal de cadena ni una consulta en el servidor merece una pregunta: ¿de dónde procede el valor? Cuentan las cookies, los campos de formulario ocultos, las cabeceras y el contenido de la base de datos. Muchos open redirects existen porque el desarrollador supuso que una cookie o un campo oculto no podían modificarse.
Para confirmar un hallazgo, no se quede en una única dirección externa sencilla. Envíe cada uno de estos valores en el parámetro y observe la cabecera Location:
http://malicious.example.comhttps://yoursite.example@malicious.example(userinfo:yoursite.examplepasa a ser un nombre de usuario)https://yoursite.example.malicious.example(un nombre de host que empieza como el suyo)https://malicious-yoursite.example(un nombre de host que termina como el suyo)
La cheat sheet menciona el userinfo y los nombres de host engañosos como formas de eludir las comprobaciones de prefijo y sufijo. Si alguno de ellos pasa cuando la URL sencilla fue bloqueada, lo probable es que la comprobación sea una simple coincidencia de cadenas. Es una inferencia, no una prueba, pero sustituya la comprobación de todos modos.
Los escáneres detectarán algunos casos. La OWASP Web Security Testing Guide señala que estas herramientas son genéricas y carecen de contexto específico de la aplicación, así que verifique cada hallazgo manualmente.
Cómo corregirlos, por orden de preferencia
- No redirija a partir de la entrada del usuario. Si el destino está fijado en el código, un atacante no puede cambiarlo.
- Asocie IDs a destinos en el servidor. El usuario proporciona un nombre corto o un ID y el servidor busca la URL completa, rechazando todo lo demás. Asegúrese de que los IDs no puedan recorrerse de forma secuencial, o un visitante podrá enumerar todos los destinos.
- Use el helper del framework para los retornos locales. En ASP.NET Core, eso significa
LocalRedirectoIsLocalUrl, que rechazan los destinos no locales. - Aplique una lista de permitidos (allowlist) para los destinos externos. Use un parser de URLs mantenido y compare el esquema, el host canónico y el puerto efectivo analizados con una lista explícita. Restrinja la ruta si solo se permiten endpoints concretos.
- Opcionalmente, exija un nonce. CWE describe un nonce impredecible, generado por la aplicación, en cada petición de redirección, rechazando las peticiones que no lo incluyan. El XSS puede eludirlo, por lo que complementa una corrección, pero no la sustituye.
Reglas para la comprobación con allowlist:
- Rechace el userinfo y las entradas ambiguas.
- Redirija usando la URL validada, no una versión que transforme después.
- No use nunca coincidencia bruta de prefijo o sufijo. La cheat sheet cita a GitLab como un caso en el que el userinfo y los nombres de host engañosos derrotaron este tipo de comprobaciones.
Una lista de bloqueo de valores maliciosos es una defensa principal débil, aunque CWE la acepta para detectar ataques. Utilice en su lugar el enfoque de "aceptar solo lo conocido como válido".
Si los usuarios deben salir de su sitio, una página intermedia que indique el destino y exija un clic añade una capa más. Evite el XSS al construir esa página.
El lado de OAuth
RFC 9700 exige que los servidores de autorización comparen las URIs de redirección de los clientes con las registradas previamente mediante coincidencia exacta de cadenas, salvo por los números de puerto en las URIs de redirección localhost de las apps nativas. Mi interpretación, que no es texto del RFC: la coincidencia exacta demuestra que el callback es el registrado, así que si ese host registrado también expone un open redirector, el código puede seguir viajando a otro destino. El RFC remite a su sección 4.11 para el detalle. Léala antes de fiarse de mi resumen.
Por dónde empezar
- Ejecute los patrones de grep anteriores sobre su código y haga una lista de todas las redirecciones y forwards cuyo destino proceda de una petición, una cookie o un valor almacenado. Revise primero los handlers de inicio de sesión y de callback de OAuth.
- Compruebe si a las coincidencias en PHP les falta el
exity si los proyectos .NET tienen acciones de login de MVC 1 o 2. - Pruebe cada coincidencia con las cuatro entradas anteriores, incluida la forma con userinfo, y anote cuáles envían el navegador fuera de su dominio.
- Sustituya las URLs proporcionadas por el usuario por mapeo de IDs en el servidor siempre que pueda, por helpers del framework solo locales para las direcciones de retorno y por una allowlist sobre URLs analizadas para los pocos destinos externos que necesite.
- Revise los registros de sus clientes OAuth para confirmar que usan URIs de redirección exactas y que ninguno de sus hosts expone un open redirector.
- Incorpore una prueba de redirecciones a la revisión de código y al alcance de sus pruebas de penetración, para que el fallo no vuelva a aparecer.
