El path traversal existe allí donde su código usa una entrada externa para construir una ruta y no impide que esa ruta se resuelva fuera del directorio previsto. CWE-22 es la entrada que lo recoge. Localice cada uno de esos puntos, deje de permitir que los usuarios indiquen nombres de archivo y compruebe la ruta resuelta para cualquier entrada que quede.
Qué obtiene un atacante
La lectura es solo el principio. Según lo que haga el código con la ruta, un atacante puede leer archivos inesperados y exponer datos sensibles, crear o sobrescribir programas y bibliotecas y, con ello, ejecutar código, o corromper archivos hasta que el producto deje de funcionar o los usuarios queden bloqueados. OWASP incluye el código fuente de la aplicación y la configuración entre los archivos a los que se puede llegar.
Los archivos protegidos por un mecanismo de seguridad son los peores objetivos. Leer un archivo de contraseñas permite ataques de fuerza bruta contra las cuentas, y añadir una cuenta nueva a uno de ellos puede saltarse la autenticación.
El alcance depende de la plataforma. En Windows un atacante solo puede moverse dentro de la partición que contiene la raíz web; en Linux, por todo el disco. Los controles de acceso del sistema operativo siguen aplicándose por encima.
Cómo encontrar los lugares que construyen rutas
Empiece por el sink, no por la URL. Haga una lista de cada llamada que abre, lee, sirve, incluye o escribe un archivo y rastree cada argumento de ruta hasta su origen. Todo lo que toque la petición va a la lista, incluidas las llamadas ocultas tras sus propias funciones envoltorio y los helpers de framework, así que revise también a quienes las invocan.
La entrada es más amplia que la query string. Los ejemplos de OWASP incluyen get-files.jsp?file=report.pdf, get-page.php?home=aaa.html y some-page.asp?page=index.html, y también una cookie. Este fragmento está adaptado del ejemplo de OWASP (originalmente de Wikipedia):
$template = 'blue.php';
if ( isset( $_COOKIE['TEMPLATE'] ) )
$template = $_COOKIE['TEMPLATE'];
include ( "/home/users/phpguru/templates/" . $template );
Una cookie con TEMPLATE=../../../../../../../../../etc/passwd hace que include() suba hasta el directorio raíz y devuelva el archivo de contraseñas. Los parámetros llamados file, page, template o f son los sospechosos habituales, pero los nombres no son una guía fiable. Rastree los datos.
Compruebe tres formas:
- Traversal relativo: secuencias
../que salen del directorio previsto. - Rutas absolutas:
?f=/etc/passwdo?f=/var/www/html/get.php, que funcionan cuando la entrada se usa como ruta completa. - Includes remotos:
?page=http://other-site/...cuando el mecanismo de inclusión acepta URLs.
Los mensajes de error detallados ayudan al atacante, porque revelan rutas reales sobre las que probar. Compruebe qué devuelve su aplicación cuando falta un archivo.
Por qué fallan las listas de bloqueo
Eliminar ../ no funciona, porque el mismo traversal admite muchas grafías:
%2e%2e%2f,..%2fy%2e%2e/significan todos../...%5cy%2e%2e%5csignifican..\, algo relevante en Windows, donde funcionan ambos separadores.%252e%252e%255ces la forma con doble codificación de..\. Los contenedores web realizan un nivel de decodificación, de modo que llega todavía codificada. Si su código o una biblioteca la decodifica de nuevo, se convierte en traversal después de que la comprobación se haya ejecutado...%c0%afy..%c1%9cson otras codificaciones de../y..\.- Un byte nulo, como en
?file=secret.doc%00.pdf, puede hacer que una aplicación Java vea.pdfmientras el sistema operativo ve.doc. Los atacantes lo usan para saltarse la validación.
El filtrado también resulta contraproducente por sí mismo. Si elimina ../ de forma secuencial en .../...//, los caracteres restantes vuelven a formar ../. Filtrar solo / no sirve de nada si el sistema de archivos acepta \.
Primera corrección: deje de aceptar rutas
El mejor control es sacar la entrada del usuario de la ruta. Use índices en lugar de partes de nombres de archivo. El ejemplo de OWASP es un archivo de idioma: el usuario envía 5 y el servidor lo asigna al idioma correspondiente, en lugar de fiarse de un nombre recibido.
Para las descargas, almacene los archivos bajo un ID y sírvalos por ID, de modo que el usuario nunca indique un nombre ni una extensión. El ejemplo de la plantilla queda así:
$templates = [1 => 'blue.php', 2 => 'red.php'];
$id = (int) ($_COOKIE['TEMPLATE'] ?? 1);
$template = $templates[$id] ?? 'blue.php';
include '/home/users/phpguru/templates/' . $template;
Si necesita aceptar un nombre, valídelo contra valores conocidos como válidos y rechace el resto. Use una lista de permitidos estricta de caracteres, como máximo un . en el nombre de archivo, ningún separador de directorios y una lista de extensiones permitidas. Rechace la entrada incorrecta en lugar de intentar sanearla.
Segunda corrección: canonice y después compruebe
Cuando la entrada deba llegar a una API de archivos, decodifíquela una sola vez y resuélvala con una función de canonicalización integrada antes de validarla:
| Lenguaje | Función |
|---|---|
| C | realpath() |
| Java | getCanonicalPath() |
| ASP.NET | GetFullPath() |
| Perl | realpath() o abs_path() |
| PHP | realpath() |
Estas funciones eliminan las secuencias .. y los enlaces simbólicos. Una vez resuelta la ruta, confirme que el resultado sigue dentro de su directorio base y rechace la petición si no es así. Un esbozo en PHP, con $name ya validado contra la lista de permitidos:
$base = realpath('/home/users/phpguru/templates');
$full = realpath($base . '/' . $name);
if ($full === false || strpos($full, $base . DIRECTORY_SEPARATOR) !== 0) {
http_response_code(404);
exit;
}
Mantenga todas las comprobaciones en el servidor. La validación en el cliente se elude con total facilidad.
Limite el daño
Mantenga los archivos de configuración sensibles fuera de la raíz web. En Windows con IIS, mantenga la raíz web fuera del disco del sistema para que el traversal no pueda llegar a los directorios del sistema. Las jaulas chroot y las políticas de acceso al código restringen dónde se pueden leer o escribir archivos, y limitan el daño cuando se escapa alguna comprobación.
Para las pruebas, repita las codificaciones anteriores contra cada parámetro que haya encontrado, junto con una ruta absoluta y una URL remota. Un test de intrusión lo hará de forma sistemática.
Por dónde empezar
- Haga una lista de cada llamada de gestión de archivos de su código, añada las funciones envoltorio y los helpers de framework que las rodean y rastree cada argumento de ruta hasta su origen, incluidas las cookies y las cabeceras.
- Sustituya los nombres de archivo proporcionados por el usuario por IDs asociados a archivos en el servidor, empezando por las descargas y la selección de plantillas o idiomas.
- Para cualquier caso restante, decodifique una sola vez, canonice con la función de la plataforma y rechace todo lo que se resuelva fuera del directorio base.
- Elimine el código de lista de bloqueo que quita
../o/y sustitúyalo por una lista de permitidos de caracteres, extensiones y, cuando sea posible, valores. - Mueva la configuración sensible fuera de la raíz web y deje de devolver mensajes de error que revelen rutas de archivos.
- Pruebe cada parámetro con las variantes codificadas, con doble codificación, absolutas y remotas, y conserve los casos como pruebas de regresión.
