Directrices

Inyección - Path Traversal

Path Traversal es otro tipo bastante común de vulnerabilidad de inyección. Suelen producirse cuando la construcción de una URI (ya sea para una URL, una ruta de archivo u otro tipo) no garantiza correctamente que la ruta completamente resuelta no apunte fuera de la raíz de la ruta prevista

Es importante señalar que el path traversal también podría ser visto como una vulnerabilidad de *inyección* de ruta. 

El impacto de una vulnerabilidad de path traversal depende en gran medida del contexto en el que se produce el traversal, y el endurecimiento general que se ha hecho. Pero antes de entrar en eso, vamos a ver un ejemplo práctico rápido de esta vulnerabilidad para ver de qué estamos hablando:                                                                                                        

Un rápido desglose

Piensa en un endpoint de tu aplicación que sirva documentos, como plantillas de contratos u ofertas de trabajo. Podría tratarse de archivos, como PDF, estáticos en la aplicación. 

En esta situación, es posible que tenga un fragmento de código como este para obtener los archivos a petición:

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

return file.read(path);

Para demostrar cómo funciona la vulnerabilidad, también tenemos que 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:

Archivo Ruta sin resolver Vía resuelta
Privacidad.pdf /var/www/api/documents/Privacidad.pdf /var/www/api/documents/Privacidad.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

Fíjate en cómo podemos recorrer el sistema de archivos utilizando '../'. Podemos salir de la carpeta "documents", donde suelen estar los PDF, y entrar en la carpeta "/etc/", que contiene el archivo "shadow", que en Linux contiene los hash de las contraseñas. Como puedes imaginar, esto no es lo ideal. 

Observación de la transversalidad en las URL

Otra variante de path traversal puede ocurrir cuando se construyen URLs que están destinadas a interactuar con una API. Supongamos que tenemos una API con los siguientes métodos:

Patrón URL Descripción
/api/v1/order/get/{id} Obtiene detalles sobre el pedido con el ID especificado
/api/v1/pedido/borrar/{id} Elimina un pedido con el ID específico

Con la API interactúa otra aplicación que podría llamarla, por ejemplo, cuando intenta 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é ocurre ahora, en función del ID de pedido proporcionado por el usuario? A continuación se muestra la URL efectiva invocada en función de la entrada proporcionada. 

La canonicalización no suele hacerse en el lado del cliente (aunque puede hacerse), pero los servidores web canonicalizarán la petición en el formato que se ve a continuación.

Número de pedido URL real invocada
1 /api/v1/pedido/get/1
1/../../borrar/1 /api/v1/pedido/suprimir/1

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

Mitigación

Cuando se habla de atravesar rutas, existen tanto mitigaciones directas como técnicas indirectas/de defensa que pueden, y deben, aplicarse tan a menudo como sea posible. En primer lugar, veamos cómo manejar las rutas.

Mitigación directa

Cuando se trata de manejar una ruta, tenemos que entender el proceso de resolución de rutas, o canonicalización de rutas, y su importancia. 

Cuando tienes una ruta como '/var/www/api/documents/../../../../etc/shadow', está en una ruta no canónica. Si solicitas esta ruta a tu sistema de archivos, la canonizará a '/etc/shadow'. Es muy importante que no intentes abrir rutas no canónicas. Por el contrario, primero debes canonizar las rutas, verificar que apuntan sólo al archivo o carpeta deseado y luego leerlo. 

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

let resolvedPath = path.resolve(path);

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

Anti-patrón - Intentar sanear los nombres de archivo

Puede resultar tentador hacer algo así:


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

Sin embargo, este enfoque no debe utilizarse. La clave en el manejo de rutas es mirar siempre la ruta canónica. 

Mientras la ruta canónica no infrinja ninguna regla, la forma en que se construya la ruta en última instancia no tiene ninguna importancia. Tratar de desinfectar una ruta de este modo 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 con los hashes de las contraseñas en Linux. Pero realmente no hay ninguna razón por la que una aplicación deba ser capaz de leer ese archivo, u otros archivos, fuera de su raíz.

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

Ejemplos

Ahora compartiremos algunos ejemplos en varios idiomas para ayudar a demostrar las cosas un poco mejor mientras están en acción.

C# - Inseguro

Al no resolver la ruta completa, o asegurarse de que sólo utiliza la parte del nombre de archivo de una ruta, deja el código vulnerable a Path Traversal. 

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

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

C# - Seguro - canónico

En este ejemplo, nos protegemos contra el Path Traversal resolviendo la ruta completa (absoluta), y asegurándonos de que la ruta resuelta del archivo está 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 "Trying to read file outside of base folder";

var fileContents = File.ReadAllText(canonicalPath);

C# - Secure - nombre de archivo

En este ejemplo, nos protegemos contra el Path Traversal tomando sólo la parte del nombre del archivo de la ruta, asegurando que es imposible atravesar fuera de la carpeta especificada. 

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

// Utilice esta opción sólo si no permite navegar por otras subcarpetas
var fileName = Path.GetFileName("../../../../../etc/passwd");

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

Java - Inseguro

Al no resolver la ruta completa, o asegurarse de que sólo utiliza la parte del nombre de archivo de una ruta, deja el código vulnerable a Path Traversal. 

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

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

Java - Seguro - Canonical

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

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

// INSECURE: Reads /etc/passwd
Path normalizedPath  = Paths.get(baseFolder + fileName).normalize();
if(!normalizedPath.toString().startsWith(baseFolder))
{
    return "Trying to read path outside of root";
}
else
{
    List<String> lines = Files.readAllLines(normalizedPath);
}

Java - Seguro - Nombre de archivo

En este ejemplo, nos protegemos contra el Path Traversal tomando sólo la parte del nombre del archivo de la ruta, asegurando que es imposible atravesar fuera de la carpeta especificada. 

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

// Only use this if you don't allow navigating into other subfolders
String fileName = Paths.get("../../../../../etc/passwd").getFileName().toString();

// SECURE: Reads /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 asegurarse de que sólo utiliza la parte del nombre de archivo de una ruta, deja el código vulnerable a Path Traversal. 

const fs = require('fs');

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

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

Javascript - Seguro - Canonical

En este ejemplo, nos protegemos contra el Path Traversal resolviendo la ruta completa (absoluta), y asegurándonos de que la ruta resuelta del archivo está 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));

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

Javascript - Seguro - Nombre de archivo

En este ejemplo, nos protegemos contra el Path Traversal tomando sólo la parte del nombre del archivo de la ruta, asegurando que es imposible atravesar fuera 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 asegurarse de que sólo utiliza la parte del nombre de archivo de una ruta, deja el código vulnerable a Path Traversal. 

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

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

Python - Seguro - Canonical

En este ejemplo, nos protegemos contra el Path Traversal resolviendo la ruta completa (absoluta), y asegurándonos de que la ruta resuelta del archivo está 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 "Trying to read out of base folder"

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

Python - Seguro - Nombre de archivo

En este ejemplo, nos protegemos contra el Path Traversal tomando sólo la parte del nombre del archivo de la ruta, asegurando que es imposible atravesar fuera 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()