Directrices

Inyección - Recorrido de rutas (Path Traversal)

El recorrido de rutas es otro tipo de vulnerabilidad de inyección bastante común. Suele ocurrir cuando la construcción de un URI (ya sea para una URL, una ruta de archivo o cualquier otro elemento) no garantiza correctamente que la ruta resuelta no apunte fuera de la raíz de la ruta prevista.

Es importante señalar que el recorrido de rutas también puede considerarse, de hecho, una vulnerabilidad de *inyección* de rutas.

El impacto de una vulnerabilidad de recorrido de rutas depende en gran medida del contexto en el que se produzca y del endurecimiento general que se haya aplicado. Pero antes de entrar en eso, veamos un ejemplo práctico rápido de esta vulnerabilidad para entender a qué nos referimos:

Un breve análisis

Considere un punto de conexión en su aplicación que sirve documentos, como plantillas para contratos u ofertas de trabajo. Todos estos podrían ser archivos, como archivos PDF, que son estáticos en su aplicación.

En esta situación, podría tener un fragmento de código como este para recuperar los archivos bajo demanda:

let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;

return file.read(path);

Para demostrar cómo se desarrolla la vulnerabilidad, también debemos saber dónde está la raíz de nuestra aplicación, así que, para este ejemplo, supongamos que la raíz de la aplicación está en "/var/www/api/".

Sabemos que la aplicación toma un parámetro "filename"; veamos algunos ejemplos de entradas y cuál es el resultado:

Filename Unresolved path Resolved path
Privacy.pdf /var/www/api/documents/Privacy.pdf /var/www/api/documents/Privacy.pdf
../config/prod.config /var/www/api/documents/../config/prod.config /var/www/api/config/prod.config
../../../../etc/shadow /var/www/api/documents/../../../../etc/shadow /etc/shadow

Observe cómo podemos recorrer el sistema de archivos usando "../". Podemos salir de la carpeta "documents", donde normalmente residen los archivos PDF, y entrar en la carpeta "/etc/", que contiene el archivo "shadow", el cual, en Linux, contiene hashes de contraseñas. Como puede imaginar, eso no es nada ideal.

Análisis del recorrido en URLs

Otra variante del recorrido de rutas puede ocurrir al construir URLs destinadas a interactuar con una API. Supongamos que tenemos una API con los siguientes métodos:

URL pattern Description
/api/v1/order/get/{id} Gets details about the order with the specified ID
/api/v1/order/delete/{id} Deletes an order with the specific ID

Otra aplicación interactúa con la API y podría llamarla, por ejemplo, al intentar obtener información sobre un pedido:

let apiBase = "https://my.api/api/v1";
let orderApi = apiBase + "/order/get";

let apiUrl = orderApi + request.params.orderId;

let response = http.get(apiUrl);

¿Qué sucede ahora, dependiendo del ID de pedido proporcionado por el usuario? A continuación, verá la URL efectiva invocada según la entrada proporcionada.

La normalización (canonicalization) generalmente no se realiza en el lado del cliente (aunque puede hacerse), pero los servidores web normalizarán la solicitud al formato que se muestra a continuación.

Order ID number Actual URL invoked
1 /api/v1/order/get/1
1/../../delete/1 /api/v1/order/delete/1

Con la entrada del segundo ejemplo, en lugar de recuperar el pedido con el número de ID "1", en realidad hemos invocado el método de eliminación, lo que, por supuesto, resulta en la eliminación del pedido.

Mitigaciones

Al hablar de recorrido de rutas (path traversal), existen tanto mitigaciones directas como técnicas indirectas o de defensa que pueden y deben aplicarse siempre que sea posible. Primero, veamos cómo gestionar las rutas.

Mitigación directa

Al gestionar una ruta, debemos comprender el proceso de resolución de rutas, o normalización de rutas, y su importancia.

Cuando tienes una ruta como «/var/www/api/documents/../../../../etc/shadow», se trata de una ruta no normalizada. Si solicitas esta ruta a tu sistema de archivos, este la normalizará a «/etc/shadow». Es fundamental que no intentes abrir rutas no normalizadas. En su lugar, primero debes normalizar las rutas, verificar que apunten únicamente al archivo o carpeta deseados y, entonces, leerlos.

‍

let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;

let resolvedPath = path.resolve(path);

if(!resolvedPath.startswith(baseFolder))
return "Se intentó leer fuera de la carpeta base";
else
return file.read(resolvedPath);

Antipatrón: intentar limpiar los nombres de archivo

Puede resultar tentador hacer algo como esto:


let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename.replace("../", "");
...

Sin embargo, este enfoque no debe utilizarse. La clave al gestionar rutas es observar siempre la ruta normalizada.

Mientras la ruta normalizada no infrinja ninguna regla, la forma en que se construya finalmente la ruta no tiene ninguna importancia. Intentar limpiar una ruta de esta manera es muy propenso a errores y rara vez es seguro, si es que alguna vez lo es.

Limitar el acceso

En nuestros ejemplos anteriores, hemos utilizado la lectura del archivo «/etc/shadow», que es el archivo que contiene los hashes de contraseñas en Linux. Pero, en realidad, no hay ninguna razón por la que una aplicación deba poder leer ese archivo, u otros archivos, fuera de su directorio raíz.

Si utilizas contenedores, es probable que ya estés mitigando muchos riesgos. Tomar medidas para proteger el contenedor (no ejecutar como root, etc.) es vital. Se recomienda encarecidamente eliminar todos los privilegios de tu proceso web y limitar sus permisos de lectura en el sistema de archivos únicamente a aquellos archivos que necesite estrictamente.

Ejemplos

Ahora compartiremos algunos ejemplos en varios lenguajes para ayudar a demostrar mejor cómo funcionan en la práctica.

C# - Inseguro

Al no resolver la ruta completa o no asegurarse de utilizar únicamente la parte del nombre de archivo de una ruta, el código queda vulnerable al recorrido de rutas (Path Traversal).

var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";

// INSEGURO: Lee /etc/passwd
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));

C# - Seguro - normalizado

En este ejemplo, nos protegemos contra el recorrido de rutas resolviendo la ruta completa (absoluta) y asegurándonos de que la ruta resuelta del archivo se encuentre dentro de nuestra carpeta base.

var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";

var canonicalPath = Path.GetFullPath(Path.Combine(baseFolder, fileName));

// SEGURO: Rechaza cualquier intento de lectura fuera de la base especificada.
if(!canonicalPath.StartsWith(baseFolder))
    return "Intento de lectura de archivo fuera de la carpeta base";

var fileContents = File.ReadAllText(canonicalPath);

C# - Seguro - nombre de archivo

En este ejemplo, nos protegemos contra el recorrido de rutas (Path Traversal) tomando solo la parte del nombre del archivo, asegurando que sea imposible salir de la carpeta especificada. 

var baseFolder = "/var/www/app/documents/";

// Utilizar solo si no se permite navegar a otras subcarpetas
var fileName = Path.GetFileName("../../../../../etc/passwd");

// SEGURO: Lee /var/www/app/documents/passwd
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));

Java - Inseguro

Al no resolver la ruta completa o no asegurarse de utilizar únicamente la parte del nombre de archivo de una ruta, el código queda vulnerable al recorrido de rutas (Path Traversal).

String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";

// INSEGURO: Lee /etc/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);

Java - Seguro - Canónico

En este ejemplo, nos protegemos contra el recorrido de rutas resolviendo la ruta completa (absoluta) y asegurándonos de que la ruta resuelta del archivo se encuentre dentro de nuestra carpeta base.

String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";

// INSEGURO: Lee /etc/passwd
Path normalizedPath  = Paths.get(baseFolder + fileName).normalize();
if(!normalizedPath.toString().startsWith(baseFolder))
{
    return "Intento de lectura de ruta fuera de la raíz";
}
else
{
    List<String> lines = Files.readAllLines(normalizedPath);
}

Java - Seguro - Nombre de archivo

En este ejemplo, nos protegemos contra el recorrido de rutas (Path Traversal) tomando solo la parte del nombre del archivo, asegurando que sea imposible salir de la carpeta especificada. 

String baseFolder = "/var/www/app/documents/";

// Utilizar solo si no se permite navegar a otras subcarpetas
String fileName = Paths.get("../../../../../etc/passwd").getFileName().toString();

// SEGURO: Lee /var/www/app/documents/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);

Javascript - Inseguro

Al no resolver la ruta completa o no asegurarse de utilizar únicamente la parte del nombre de archivo de una ruta, el código queda vulnerable al recorrido de rutas (Path Traversal).

const fs = require('fs');

const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";

// INSEGURO: Lee /etc/passwd
const data = fs.readFileSync(baseFolder + fileName, 'utf8');

Javascript - Seguro - Canónico

En este ejemplo, nos protegemos contra el recorrido de rutas resolviendo la ruta completa (absoluta) y asegurándonos de que la ruta resuelta del archivo se encuentre dentro de nuestra carpeta base.

const fs = require("fs");
const path  = require("path");

const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";

const normalizedPath = path.normalize(path.join(baseFolder, fileName));

// SEGURO: Lee /var/www/app/documents/passwd
const data = fs.readFileSync(normalizedPath, 'utf8');

Javascript - Seguro - Nombre de archivo

En este ejemplo, nos protegemos contra el recorrido de rutas (Path Traversal) tomando solo la parte del nombre del archivo, asegurando que sea imposible salir de la carpeta especificada. 

const fs = require("fs");
const path  = require("path");

const baseFolder = "/var/www/app/documents/";
const fileName = path.basename("../../../../../etc/passwd");

// SEGURO: Lee /var/www/app/documents/passwd
const data = fs.readFileSync(path.join(baseFolder, fileName), 'utf8');

Python - Inseguro

Al no resolver la ruta completa o no asegurarse de utilizar únicamente la parte del nombre de archivo de una ruta, el código queda vulnerable al recorrido de rutas (Path Traversal).

baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"

# INSEGURO: Lee /etc/passwd
fileContents = open(baseFolder + fileName).read()

Python - Seguro - Canónico

En este ejemplo, nos protegemos contra el recorrido de rutas resolviendo la ruta completa (absoluta) y asegurándonos de que la ruta resuelta del archivo se encuentre dentro de nuestra carpeta base.

import os.path

baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"

normalizedPath =  os.path.normpath(baseFolder + fileName)

# SEGURO: Rechaza cualquier intento de leer archivos fuera de la carpeta base especificada
if not normalizedPath.startswith(baseFolder):
    return "Intentando leer fuera de la carpeta base"

# SEGURO: Lee /var/www/app/documents/passwd
fileContents = open(normalizedPath).read()

Python - Seguro - Nombre de archivo

En este ejemplo, nos protegemos contra el recorrido de rutas (Path Traversal) tomando solo la parte del nombre del archivo, asegurando que sea imposible salir de la carpeta especificada. 

import os.path

baseFolder = "/var/www/app/documents/"
fileName = os.path.basename("../../../../../etc/passwd")

# SEGURO: Lee /var/www/app/documents/passwd
fileContents = open(os.path.join(baseFolder,  fileName)).read()
Eslogan

Programación segura y práctica

Explore las directrices de programación segura para comprender y mitigar vulnerabilidades de software como el OWASP Top 10, y sumérjase en misiones de formación guiada para practicar en simulaciones de aplicaciones del mundo real.

Más directrices
Reservar una demostración