Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 078 — Dependencias vulnerables#

⬅️ 077 · 📚 Parte 5 · 🎓 Clases · 079 ➡️ Parte 5 — Identidad y seguridad · Nivel 🟡 intermedio · Pista plataformaClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Saber qué arrastras y cuándo te expone. Las siete clases anteriores protegieron el código que escribes; esta mira el código que no escribes y que sin embargo ejecutas — que en cualquier aplicación moderna es la inmensa mayoría [owasp-top10].

🧩 La situación#

Una aplicación declara dos dependencias y ejecuta seis. Una de las cuatro que nadie eligió tiene un aviso de seguridad conocido, crítico y explotado activamente: CVE-2017-5638, la ejecución remota de código en el analizador multipart de Apache Struts 2 [cve-2017-5638] — la misma que está detrás de la brecha de Equifax de 2017 [struts-security].

🧮 El contrato#

Petición Respuesta Qué mide
GET /dependencias directas: 2, total: 6 el árbol es mayor que tu manifiesto
GET /dependencias/struts2-core directa: false, traida_por: ["portal-base"] nunca la elegiste: la trajo otra
GET /auditoria afectadas: 1, con CVE-2017-5638 y 2.5.10.1 el aviso se localiza y se nombra
GET /auditoria el hallazgo es directa: false y se dice que es transitiva
GET /auditoria?version=2.5.13 afectadas: 0 actualizar cierra el aviso
GET /auditoria?version=2.5.9 afectadas: 1 la comparación es numérica

El último caso es el que convierte esta clase en una medición y no en una declaración de intenciones. 2.5.9 está afectada porque 9 < 10, pero alfabéticamente "2.5.9" > "2.5.10": un auditor que compare versiones como texto declara sana una versión vulnerable. Es un fallo silencioso, no lanza ninguna excepción, y produce exactamente lo peor que puede producir una herramienta de seguridad: un verde falso.

Y el quinto y el sexto juntos prueban que la auditoría compara en lugar de llevar la respuesta escrita: la misma petición con distinta versión da distinto veredicto en las dos direcciones.

🔬 Qué es dato congelado aquí, y por qué#

Con la misma honestidad que la clase 077:

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Archivo de bloqueo (Lockfile) El archivo que la herramienta genera junto al manifiesto con la versión exacta de cada dependencia y de las dependencias de tus dependencias. Es lo que hace que la instalación de hoy sea igual a la de dentro de seis meses. Se versiona; no se edita a mano.
Dependencia transitiva Una dependencia que no elegiste: la trajo otra que sí elegiste. Una aplicación que declara dos suele ejecutar seis, y un aviso de seguridad sobre una transitiva no se arregla actualizándola a ella — o subes a quien la trajo, o fuerzas la versión.
Versionado semántico (SemVer) La convención MAYOR.MENOR.PARCHE: el primer número anuncia una ruptura, el segundo una función nueva compatible y el tercero una corrección. Los rangos del manifiesto —^5.1.0— declaran hasta dónde aceptas actualizaciones automáticas.
Cadena de suministro Todo el código que ejecutas y no escribiste, incluidas las dependencias de tus dependencias y los scripts que se ejecutan al instalarlas. Este repositorio instala con --ignore-scripts por eso.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
ASP.NET Core framework web de .NET (C#) 2016 MIT Microsoft y .NET Foundation

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
datos/arbol.json datos en JSON usados por la implementación
datos/avisos.json datos en JSON usados por la implementación
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
datos/arbol.json datos en JSON usados por la implementación
datos/avisos.json datos en JSON usados por la implementación
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-078-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
datos/arbol.json datos en JSON usados por la implementación
datos/avisos.json datos en JSON usados por la implementación
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java
src/main/resources/application.properties configuración de Spring Boot: lo que se ajusta sin tocar el código

🔧 ASP.NET Core#

Reescritura multiplataforma y de código abierto de la pila web de Microsoft. Sus API mínimas trajeron el estilo de los microframeworks al ecosistema .NET.

Preparar sus dependencias, dentro de su directorio:

dotnet build -c Release --nologo -v quiet

Arrancarla suelta, sin el verificador:

PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000

Qué hay dentro de su directorio:

Archivo Qué es
Clase078.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
datos/arbol.json datos en JSON usados por la implementación
datos/avisos.json datos en JSON usados por la implementación
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Las cuatro implementan el mismo auditor en cuatro lenguajes: leer el árbol, cruzarlo con los avisos, comparar versiones numéricamente y decir quién trajo la afectada. La uniformidad es deliberada — como en la 069, lo que se compara no es el código sino lo que cada ecosistema pone alrededor.

Y el corazón del auditor son quince líneas que deciden si la herramienta sirve o miente.

La comparación de versiones · express/server.mjs#

function menorQue(a, b) {
  const pa = String(a).split(".").map(Number);
  const pb = String(b).split(".").map(Number);
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
    const x = pa[i] ?? 0;
    const y = pb[i] ?? 0;
    if (x !== y) return x < y;
  }
  return false;
}

Comparar versiones como texto es el error que convierte una auditoría en un tranquilizante. "2.5.9" > "2.5.10" es cierto alfabéticamente, así que una comparación textual declararía sana una versión afectada — sin excepción, sin registro, produciendo lo peor que puede producir una herramienta de seguridad: un verde falso.

Y el ?? 0 no es defensivo por costumbre: un componente que falta cuenta como cero, para que 2.5.10 < 2.5.10.1.

FastAPI · fastapi/main.py#

    ta, tb = _tupla(a), _tupla(b)
    largo = max(len(ta), len(tb))
    ta = ta + (0,) * (largo - len(ta))
    tb = tb + (0,) * (largo - len(tb))
    return ta < tb

Python es el único de los cuatro que no necesita el bucle: rellena las dos tuplas hasta la misma longitud y deja que la comparación de tuplas haga el recorrido componente a componente. Menos código, y sobre todo menos sitio donde equivocarse.

Spring Boot · spring-boot/…/Aplicacion.java#

    private static boolean menorQue(String a, String b) {
        String[] pa = a.split("\\.");
        String[] pb = b.split("\\.");
        int largo = Math.max(pa.length, pb.length);
        for (int i = 0; i < largo; i++) {
            int x = i < pa.length ? Integer.parseInt(pa[i]) : 0;
            int y = i < pb.length ? Integer.parseInt(pb[i]) : 0;
            if (x != y) {
                return x < y;
            }
        }
        return false;
    }

Idéntico al de Express con un detalle propio del lenguaje: split("\\.") lleva el punto escapado porque String.split recibe una expresión regular, no un separador literal. Un split(".") a secas devuelve un array vacío y el auditor diría que ninguna versión está afectada.

ASP.NET Core · aspnet-core/Program.cs#

static bool MenorQue(string a, string b)
{
    var pa = a.Split('.');
    var pb = b.Split('.');
    for (var i = 0; i < Math.Max(pa.Length, pb.Length); i++)
    {
        var x = i < pa.Length ? int.Parse(pa[i]) : 0;
        var y = i < pb.Length ? int.Parse(pb[i]) : 0;
        if (x != y) return x < y;
    }
    return false;
}

Split('.') con un carácter, no con una cadena: C# distingue los dos y aquí la sobrecarga correcta es literal. La misma función, y la trampa de Java no existe.

Lo que hace accionable un hallazgo#

      directa: paquete.directa,
      traida_por: paquete.traida_por,
      explotada_activamente: aviso.explotada_activamente === true,

Un hallazgo sin estos tres campos es una alarma; con ellos es una tarea. Sobre una dependencia transitiva no se actualiza: o subes a quien la trajo, o fuerzas la versión — y saber cuál de las dos requiere saber quién la trajo.

    const forzada = peticion.query.version;

Y ?version= permite preguntar «¿y si actualizo?» sin tocar el árbol. Es la pregunta que se hace antes de planificar la actualización, y es también lo que demuestra que el auditor compara en vez de llevar la respuesta escrita: la misma petición con distinta versión da distinto veredicto en las dos direcciones.

⚠️ Los dos ficheros de datos/ son datos congelados, no software instalado: el árbol de una aplicación de 2017 y una instantánea de la base de avisos. Este laboratorio no instala bibliotecas vulnerables — audita datos sobre ellas, que es literalmente lo que hace un auditor. Y congelar la base es lo que hace repetible el contrato: atado a un feed vivo daría verdes distintos cada semana sin que el código cambiara.

📊 Comparación#

Lo que trae de serie cada ecosistema para hacer esto de verdad:

Ecosistema La herramienta Dónde vive el árbol resuelto
Node.js npm audit / pnpm audit, integrado en el gestor package-lock.json, pnpm-lock.yaml
Python pip-audit (PyPA), externo al gestor requirements.txt fijado, poetry.lock, uv.lock
JVM OWASP Dependency-Check, complemento de Maven/Gradle resolución de Maven (mvn dependency:tree)
.NET dotnet list package --vulnerable, en el propio SDK packages.lock.json

Node y .NET lo traen dentro de la herramienta que ya usas; Python y la JVM lo dejan en un complemento que hay que añadir y recordar. La diferencia no es de calidad del análisis sino de fricción, y en seguridad la fricción decide: lo que no está a un comando de distancia no se ejecuta.

Y una condición previa que atraviesa las cuatro: sin fichero de bloqueo no hay auditoría posible. Un ^4.17.0 no identifica una versión, y sobre un rango no se puede pronunciar ningún aviso. Auditar sin lockfile es auditar la mitad del árbol y llamarlo entero.

🧭 Un hallazgo no es una alarma#

El resultado de una auditoría no es una lista de tareas: es una lista de preguntas. Lo que decide la urgencia:

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 078

Los casos están en contrato.json. El verificador ejecuta las implementaciones que encuentre y declara las que omitió.

🧪 Reto de transferencia#

Añade a la base un aviso con rango (introducida_en y fijada_en) en lugar de solo el límite superior, y el caso que lo mide: una versión anterior a introducida_en no está afectada. Es el error opuesto al del sexto caso —el falso positivo— y el que hace que los equipos dejen de mirar los informes.

🔗 Enlaces#

Fuentes#