Saltar al contenido
Lección 7 de 8

SSRF y Componentes Vulnerables

10 min read

Server-Side Request Forgery (SSRF)

El SSRF (Server-Side Request Forgery, CWE-918) es una vulnerabilidad que ganó tanta relevancia que el OWASP Top 10 le dedica su propia categoría (A10:2021). Ocurre cuando una aplicación obtiene un recurso remoto a partir de una URL proporcionada por el usuario sin validarla. El atacante consigue que el servidor realice peticiones en su nombre, usando la posición privilegiada del servidor dentro de la red.

Lo que hace al SSRF tan peligroso es que el servidor suele tener acceso a recursos internos que el atacante no puede alcanzar directamente: servicios en la red interna, bases de datos, paneles de administración o, en entornos de nube, los endpoints de metadatos que exponen credenciales temporales de la instancia. Un SSRF puede así convertirse en la puerta de entrada a la red interna o en el robo de credenciales de nube, escalando de un parámetro aparentemente inofensivo a un compromiso serio de la infraestructura.

El impacto real: metadatos de la nube

En un proveedor de nube, cada instancia dispone de un servicio de metadatos accesible únicamente desde dentro de la propia instancia, en una dirección IP de enlace local reservada. Ese servicio entrega información de configuración y, en muchos casos, credenciales temporales asociadas al rol de la instancia. Si una función vulnerable a SSRF permite que el atacante dirija una petición del servidor hacia ese endpoint interno, puede leer esas credenciales y actuar con los permisos de la instancia sobre otros recursos de la cuenta.

Una de las brechas de datos más grandes de los últimos años en la nube siguió exactamente ese patrón: un SSRF permitió alcanzar el servicio de metadatos, obtener credenciales de instancia y, con ellas, acceder a datos almacenados de millones de personas. No hace falta memorizar el caso; lo importante es entender la cadena: entrada de URL no validada → petición del servidor a un recurso interno → acceso privilegiado. Romper cualquier eslabón de esa cadena mitiga el riesgo.

Ejemplo: SSRF vulnerable y su corrección

Consideremos un endpoint que descarga una imagen a partir de una URL enviada por el usuario. La versión ingenua confía por completo en la entrada:

// VULNERABLE: la URL del usuario se usa sin ninguna validación
import express from "express";

const app = express();

app.get("/fetch-image", async (req, res) => {
  const { url } = req.query;
  const response = await fetch(url); // el servidor pide lo que sea que le manden
  const body = await response.arrayBuffer();
  res.type("image/png").send(Buffer.from(body));
});

El problema es que url puede apuntar a un servicio interno, a localhost o al endpoint de metadatos. La corrección aplica defensa en profundidad: lista de permitidos de dominios, resolución y validación de la IP de destino, restricción de protocolos y bloqueo de redirecciones automáticas.

// SEGURO: allow-list de dominios, validación de IP y protocolo, sin redirecciones
import express from "express";
import dns from "node:dns/promises";
import net from "node:net";
import ipaddr from "ipaddr.js";

const app = express();

const ALLOWED_HOSTS = new Set(["images.example.com", "cdn.example.com"]);
const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);

async function isSafeUrl(rawUrl) {
  let parsed;
  try {
    parsed = new URL(rawUrl);
  } catch {
    return false; // entrada que ni siquiera es una URL válida
  }

  // 1. Solo http/https
  if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) return false;

  // 2. Allow-list estricta de dominios
  if (!ALLOWED_HOSTS.has(parsed.hostname)) return false;

  // 3. Resolver la IP y bloquear rangos internos/loopback
  const { address } = await dns.lookup(parsed.hostname);
  if (!net.isIP(address)) return false;
  const range = ipaddr.parse(address).range();
  const blocked = ["private", "loopback", "linkLocal", "uniqueLocal", "reserved"];
  if (blocked.includes(range)) return false;

  return true;
}

app.get("/fetch-image", async (req, res) => {
  const { url } = req.query;

  if (typeof url !== "string" || !(await isSafeUrl(url))) {
    return res.status(400).json({ error: "URL no permitida" });
  }

  const response = await fetch(url, { redirect: "manual" }); // sin seguir redirecciones
  const body = await response.arrayBuffer();
  res.type("image/png").send(Buffer.from(body));
});

Cada capa cubre un hueco de la anterior: la lista de permitidos limita a dónde puede ir la petición, la resolución de IP evita que un dominio permitido pero comprometido o un truco de DNS apunte a un rango interno, y redirect: "manual" impide que un servidor externo redirija la petición hacia localhost o los metadatos.

Cómo Prevenir el SSRF

La defensa más robusta contra el SSRF es no confiar nunca en URLs proporcionadas por el usuario para realizar peticiones del lado del servidor. Cuando la funcionalidad lo requiera, valida la URL con una lista de permitidos estricta: solo dominios y rutas explícitamente autorizados, en lugar de intentar bloquear los peligrosos con una lista de denegados, que siempre se puede eludir.

Como capas adicionales, resuelve y valida la dirección IP de destino para bloquear rangos internos y de loopback, deshabilita las redirecciones automáticas o valídalas también, y restringe los protocolos permitidos a http y https. A nivel de red, aplica segmentación y reglas de firewall de salida para que, aunque el SSRF ocurra, el servidor no pueda alcanzar servicios internos sensibles. En la nube, usa la versión más reciente y endurecida del servicio de metadatos. La combinación de validación de entrada y segmentación de red es la defensa en profundidad adecuada.

Deserialización Insegura

La deserialización insegura (CWE-502) ocurre cuando una aplicación reconstruye objetos a partir de datos serializados que provienen de una fuente no confiable. La serialización convierte objetos en un formato transmisible; la deserialización los reconstruye. Si un atacante puede manipular los datos serializados, en algunos lenguajes y bibliotecas puede manipular qué objetos se crean y qué métodos se invocan durante el proceso de reconstrucción.

El impacto puede llegar hasta la ejecución remota de código en el servidor, uno de los peores escenarios posibles. Un ejemplo clásico es usar pickle de Python sobre datos que llegan de la red o del cliente:

# VULNERABLE: pickle reconstruye objetos arbitrarios desde datos no confiables
import pickle

def load_session(raw_bytes):
    # raw_bytes proviene de una cookie, un mensaje o un archivo subido
    return pickle.loads(raw_bytes)  # puede ejecutar código durante la deserialización

pickle no está diseñado para datos no confiables: durante la reconstrucción puede invocar comportamiento definido por el propio dato. La corrección es usar un formato puramente de datos con un esquema estricto, que solo produce estructuras conocidas (diccionarios, listas, números y cadenas):

# SEGURO: JSON solo produce datos, más validación de esquema estricta
import json
from pydantic import BaseModel, ValidationError

class Session(BaseModel):
    user_id: int
    role: str
    expires_at: int

def load_session(raw_text):
    try:
        data = json.loads(raw_text)      # nunca instancia objetos arbitrarios
        return Session(**data)           # valida tipos y campos esperados
    except (json.JSONDecodeError, ValidationError):
        raise ValueError("Sesión inválida")

La prevención principal es evitar deserializar datos no confiables siempre que sea posible. Cuando necesites intercambiar datos, prefiere formatos como JSON con esquemas estrictos, en lugar de la serialización nativa de objetos del lenguaje. Si realmente debes deserializar objetos, firma criptográficamente los datos para verificar su integridad antes de procesarlos, restringe los tipos que se pueden deserializar a una lista conocida y ejecuta el proceso con los mínimos privilegios posibles.

Componentes y Dependencias Vulnerables

Las aplicaciones modernas se construyen sobre cientos de bibliotecas de terceros. El OWASP Top 10 dedica la categoría A06:2021 — Vulnerable and Outdated Components a este riesgo, y es enorme precisamente por su escala: una sola dependencia vulnerable, posiblemente una que ni siquiera sabías que tenías (una dependencia transitiva), puede comprometer toda la aplicación. Incidentes históricos en frameworks y librerías ampliamente usados afectaron a miles de organizaciones simultáneamente, muchas de las cuales ni siquiera sabían que el componente afectado formaba parte de su cadena de dependencias.

El problema se agrava porque las dependencias quedan desactualizadas silenciosamente. Una librería que era segura cuando la añadiste puede tener una vulnerabilidad descubierta meses después. Sin un proceso para detectar y actualizar estos componentes, acumulas deuda de seguridad invisible: código de terceros que nadie está vigilando y que un atacante sí revisa activamente en busca de versiones conocidas y vulnerables. Aquí entran también las vulnerabilidades de tipo cliente, donde scripts de terceros cargados en tu página pueden ser comprometidos y afectar directamente al navegador de tus usuarios.

Seguridad de la Cadena de Suministro

La seguridad de la cadena de suministro (supply chain) consiste en gestionar el riesgo que introducen todos los componentes externos de los que depende tu software. El primer paso es la visibilidad: mantén un inventario actualizado de todas tus dependencias, idealmente generando un SBOM (Software Bill of Materials) que liste cada componente y su versión, incluyendo las dependencias transitivas.

Sobre esa base, integra el análisis de composición de software (SCA) en tu pipeline de desarrollo: herramientas que comparan tus dependencias contra bases de datos de vulnerabilidades conocidas (CVE) y te alertan cuando aparece una nueva. Automatiza las actualizaciones de seguridad, fija versiones para evitar cambios inesperados, verifica la integridad de los paquetes que descargas y reduce tu superficie eliminando dependencias que no usas.

Herramientas de Detección

  • npm audit (y sus equivalentes pip-audit, cargo audit): revisión rápida de vulnerabilidades conocidas en el árbol de dependencias, directamente desde la línea de comandos.
  • Dependabot / Renovate: automatizan la detección de versiones vulnerables y abren pull requests con las actualizaciones, integrados en el repositorio.
  • Snyk y OWASP Dependency-Check: herramientas de SCA más completas que analizan dependencias, licencias y rutas de explotación, aptas para integrar en CI/CD.
  • Burp Suite y OWASP ZAP: para probar dinámicamente el comportamiento de la aplicación, incluida la validación de las defensas contra SSRF.
  • SAST/DAST: el análisis estático revisa tu código en busca de patrones peligrosos (por ejemplo, una URL de usuario que llega sin validar a una petición), y el dinámico prueba la aplicación en ejecución.

Tu aplicación es tan segura como su componente más débil, incluso si ese componente lo escribió otra persona.

Checklist de Prevención

  • SSRF: valida toda URL de usuario contra una lista de permitidos de dominios; nunca uses listas de denegados como única defensa.
  • Resuelve la IP de destino y bloquea rangos internos, loopback y de enlace local antes de realizar la petición.
  • Restringe los protocolos a http/https y deshabilita las redirecciones automáticas o valídalas también.
  • Aplica segmentación de red y firewall de salida: el servidor no debería poder alcanzar servicios internos sensibles.
  • En la nube, usa la versión endurecida del servicio de metadatos y limita los permisos del rol de la instancia al mínimo.
  • Deserialización: evita deserializar datos no confiables; prefiere JSON con esquema estricto frente a la serialización nativa de objetos.
  • Si debes deserializar objetos, firma y verifica la integridad, restringe los tipos permitidos y ejecuta con mínimos privilegios.
  • Componentes: genera un SBOM y ejecuta SCA en cada build; mantén las dependencias actualizadas.
  • Fija versiones, verifica la integridad de los paquetes y elimina las dependencias que no usas.
  • Automatiza las alertas con npm audit, Dependabot o Snyk y trátalas como parte del flujo de trabajo, no como una tarea ocasional.