Resumen
Hay vulnerabilidades que no están en una línea de código llamativa, sino en una suposición. Aquí la suposición era: "el nombre del fichero que voy a copiar siempre apuntará a un adjunto legítimo". GitLab, al mover una incidencia de un proyecto a otro, copiaba también sus ficheros adjuntos mediante un componente llamado UploadsRewriter. El problema es que ese componente no validaba a qué fichero apuntaba realmente la referencia, y con un poco de ../ se le podía pedir cualquier cosa del disco.
Pasos de reproducción
El patrón que buscaba las referencias a adjuntos era este:
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
Fíjate en que file se captura con .*?: acepta literalmente cualquier cosa. Ahí no hay ninguna restricción de ruta, así que basta con incluir un recorrido de directorios. La reproducción es tan directa como suena:
- Crear dos proyectos.
- Añadir en el primero una incidencia cuya descripción incluya una referencia de adjunto manipulada:
- 
+ 
- Mover la incidencia al segundo proyecto.
- GitLab copia el fichero de destino —
/etc/passwden el ejemplo— como adjunto del proyecto, donde ya se puede leer tranquilamente.
Impacto
Con esto se podía leer cualquier fichero al que tuviera acceso el proceso de GitLab: tokens, ficheros de configuración, claves privadas, datos de otros usuarios… Y una lectura arbitraria de ficheros rara vez se queda en "solo lectura": suele ser el primer peldaño para robar credenciales con las que escalar a algo más serio.
Remediación
La corrección consiste en validar y resolver la ruta antes de tocar el disco, confinándola al directorio de uploads que se espera, de modo que ningún ../ pueda salir de ahí. La moraleja es la de siempre con el path traversal: una expresión regular que "captura" un nombre de fichero no lo está validando; el control tiene que hacerse sobre la ruta ya resuelta, no sobre lo que el usuario escribió.