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

Clase 046 — Filtrado y ordenación#

⬅️ 045 · 📚 Parte 3 · 🎓 Clases · 047 ➡️ Parte 3 — Validación y contrato · Nivel 🟡 intermedio · Pista backendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Aceptar criterios del cliente sin abrir un agujero. Es la clase donde una comodidad —«deja que el cliente ordene por lo que quiera»— se convierte en un problema de seguridad si se implementa como parece natural.

🧩 La situación#

GET /tareas?completada=true&orden=-prioridad filtra y ordena. Un campo que no esté en la lista blanca responde 422.

🔒 La lista blanca no es una precaución: es el mecanismo#

La forma «natural» de implementar esto es tomar el nombre del campo que envía el cliente y usarlo:

// NO HAGAS ESTO
resultado.sort((a, b) => a[campo] > b[campo] ? 1 : -1);
consulta += ` ORDER BY ${campo}`;   // y esto, menos todavía

Tres problemas, de menor a mayor gravedad:

1. Ordenar por un campo interno. ?orden=hash_contrasena no expone el valor, y expone el orden: comparando resultados se puede deducir información del campo. Es una fuga por canal lateral.

2. Filtrar por un campo que no debería ser filtrable. ?rol=admin sobre una tabla de usuarios convierte una lista pública en un directorio de administradores.

3. Inyección. Si el nombre del campo acaba concatenado en una consulta, el cliente escribe SQL. Es el vector clásico, y OWASP lo mantiene entre los riesgos principales precisamente porque sigue apareciendo [owasp-top10].

La lista blanca resuelve los tres a la vez, y por eso las cuatro implementaciones la tienen antes que cualquier otra cosa:

const CAMPOS_ORDENABLES = new Set(["titulo", "prioridad"]);
const CAMPOS_FILTRABLES = new Set(["completada", "prioridad"]);

<!-- 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
Filtrado Acotar una lista por criterios que llegan del cliente. El riesgo no es la sintaxis: es aceptar como campo de ordenación cualquier texto que llegue, que es una inyección con otro nombre.

🧰 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
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
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-046-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
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

🔧 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
Clase046.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
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 hacen lo mismo y en el mismo orden: declarar la lista blanca antes que ninguna otra cosa, y traducirla a algo que tú escribiste.

Esa segunda mitad es la que se olvida. Comprobar que el campo está en la lista y luego construir la cláusula con el texto del cliente no protege de nada: la lista blanca solo sirve si lo que llega a la consulta es un objeto tuyo.

Express · express/server.mjs#

const CAMPOS_ORDENABLES = new Set(["titulo", "prioridad"]);
const CAMPOS_FILTRABLES = new Set(["completada", "prioridad"]);

Dos listas y no una, porque son permisos distintos. Un campo puede ser filtrable y no ordenable, y al revés. Aquí titulo se ordena pero no se filtra, y completada se filtra pero no se ordena.

    if (!CAMPOS_FILTRABLES.has(campo)) {
      return respuesta.status(422).json({ code: "CAMPO_NO_FILTRABLE", campo });
    }
    if (campo === "prioridad") {
      const n = Number(valor);
      if (!Number.isInteger(n)) {
        return respuesta.status(422).json({ code: "VALOR_INVALIDO", campo });
      }
      resultado = resultado.filter((t) => t.prioridad === n);
    }

Dos comprobaciones por filtro, no una: el campo tiene que estar permitido y el valor tiene que ser del tipo correcto. Un prioridad=abc que llegue hasta la comparación devuelve una lista vacía en lugar de un error, y el cliente no se entera de que se equivocó.

    const descendente = orden.startsWith("-");
    const campo = descendente ? orden.slice(1) : orden;
    if (!CAMPOS_ORDENABLES.has(campo)) {
      return respuesta.status(422).json({ code: "CAMPO_NO_ORDENABLE", campo });
    }

-campo para descendente: una convención extendida y suficiente, y sobre todo una sola cosa que analizar. Un ?orden=titulo&direccion=desc tiene dos parámetros que pueden contradecirse.

FastAPI · fastapi/main.py#

    for campo, valor in peticion.query_params.items():
        if campo == "orden":
            continue
        if campo not in CAMPOS_FILTRABLES:
            return JSONResponse(
                {"code": "CAMPO_NO_FILTRABLE", "campo": campo}, status_code=422)
        resultado.sort(key=lambda t: t[campo], reverse=descendente)

Aquí campo sí entra en el acceso al diccionario — y es seguro porque acaba de pasar la lista blanca: solo puede valer "titulo" o "prioridad". Es la diferencia entre usar la entrada del cliente después de haberla acotado y usarla directamente.

Nótese que esta clase no declara los filtros en la firma, a diferencia de las 013 y 045. No podría: los nombres de los filtros son variables, y la firma sirve justo para lo contrario — declarar lo que se conoce de antemano.

Spring Boot · spring-boot/…/Aplicacion.java — la traducción, explícita#

    private static final Set<String> ORDENABLES = Set.of("titulo", "prioridad");
    private static final Set<String> FILTRABLES = Set.of("completada", "prioridad");
            Comparator<Tarea> comparador = "titulo".equals(campo)
                    ? Comparator.comparing(Tarea::titulo)
                    : Comparator.comparingInt(Tarea::prioridad);
            resultado.sort(descendente ? comparador.reversed() : comparador);

Este bloque es el corazón de la clase. La lista blanca no se usa para autorizar un texto: se usa para elegir entre comparadores que ya existen en el código. Tarea::titulo es una referencia a un método real; no hay ningún punto donde el texto del cliente se convierta en una expresión.

La alternativa —construir la cláusula de ordenación concatenando el nombre del campo— es la versión de este problema que acaba en inyección, y la clase 074 la mide.

comparingInt y no comparing para el número: evita empaquetar cada entero en un objeto al comparar. Un detalle de rendimiento sin consecuencias aquí y con ellas en una lista grande.

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

        Func<Tarea, object> clave = campo == "titulo" ? t => t.Titulo : t => t.Prioridad;
        resultado = descendente
            ? resultado.OrderByDescending(clave)
            : resultado.OrderBy(clave);

Exactamente lo mismo que Spring con otra sintaxis: la lista blanca se traduce a un selector conocido, no a una expresión construida con el texto del cliente.

    IEnumerable<Tarea> resultado = tareas;
            resultado = resultado.Where(t => t.Completada == esperado);

IEnumerable y Where encadenados: los filtros se componen sin ejecutarse. Sobre una consulta a base de datos, LINQ acumula las condiciones y emite una sola consulta con todos los WHERE — que es la propiedad que hace que este patrón sirva en producción y no solo con tres elementos en memoria.

📖 Y el paso que casi nadie da#

Tener la lista blanca no basta si después usas el texto del cliente. Fíjate en cómo la traducen Spring y ASP.NET:

// La lista blanca se traduce a un comparador CONOCIDO
Comparator<Tarea> comparador = "titulo".equals(campo)
        ? Comparator.comparing(Tarea::titulo)
        : Comparator.comparingInt(Tarea::prioridad);
Func<Tarea, object> clave = campo == "titulo" ? t => t.Titulo : t => t.Prioridad;

El texto del cliente nunca llega a la consulta. Solo decide qué comparador —de los que tú escribiste— se usa. Con un ORM, lo equivalente es traducir a una expresión construida por ti, no interpolar el nombre.

Es la diferencia entre validar la entrada y no usar la entrada, y la segunda es estructuralmente más segura.

🧮 El contrato#

Petición Respuesta
GET /tareas los tres
?completada=true solo el 2
?orden=titulo alfa, beta, gamma
?orden=-prioridad 3, 2, 1
?orden=id 422 · CAMPO_NO_ORDENABLE
?titulo=alfa 422 · CAMPO_NO_FILTRABLE
?completada=quizas 422 · VALOR_INVALIDO

El quinto caso es el importante. id existe en el objeto y no está en la lista blanca: se rechaza. Un filtro que solo comprobara «¿existe este campo?» lo aceptaría.

🧭 El prefijo - para descendente#

?orden=titulo     ascendente
?orden=-titulo    descendente

Es una convención extendida y suficiente. Las alternativas —orden=titulo&dir=desc, sort=titulo:desc— funcionan igual; lo que importa es elegir una y documentarla, porque el cliente no la puede adivinar.

Y cuando hagan falta varios criterios, ?orden=-prioridad,titulo se lee bien y se analiza fácil. Geewax recomienda esa forma por ser la que menos ensucia la cadena de consulta [geewax-api-design-patterns].

🔬 Comparación#

Framework Cómo traduce la lista blanca ¿Puede el texto del cliente llegar a la consulta?
Spring Boot a un Comparator tipado no
ASP.NET Core a un selector tipado no
FastAPI a una clave de ordenación solo si lo escribes mal
Express a un índice del objeto , si olvidas la lista

Los dos primeros hacen incómodo lo inseguro. En los dos últimos, a[campo] es una línea que funciona y parece razonable — y ahí la lista blanca no es una precaución añadida: es la única defensa.

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 046

🧪 Reto de transferencia#

Añade ?titulo_contiene=al como filtro de texto parcial. Después responde: ¿qué pasa si el cliente envía %, _ o una expresión regular? Es la misma pregunta de la lista blanca aplicada al valor en lugar de al campo, y tiene la misma respuesta: escapar o rechazar, nunca interpolar.

🔗 Enlaces#

Fuentes#