Seguridad de APIs · Lección 13

Validación de esquema, entrada, salida y enumeración

Una API sin validación de esquema confía en que el cuerpo que recibe tiene la forma que espera: los campos correctos, del tipo correcto, y nada más. Es la misma confianza que rompía el mass assignment, ahora desde otro ángulo: no basta con decidir qué campos se escriben, también hay que rechazar lo que no encaja antes de que llegue a la lógica de negocio. Y hay una tercera cara que casi nadie mira: lo que la API devuelve. Una respuesta que trae de más —una clave, un campo interno— o que distingue "este usuario no existe" de "la clave es incorrecta" le regala al atacante información que no necesitaba tener. Esta lección cierra las tres: entrada, salida y los oráculos de enumeración.

Al terminar sabrás

  • Definir un esquema de entrada y rechazar por tipo, forma y campos no permitidos antes de la lógica.
  • Distinguir validación de sintaxis (¿es JSON válido?) de validación de esquema (¿tiene la forma correcta?).
  • Filtrar la salida a una lista de campos públicos para no sobre-exponer datos sensibles.
  • Reconocer un oráculo de enumeración y explicarlo en el laboratorio con códigos y mensajes.
  • Uniformar respuestas y tiempos para que la API no confirme qué usuarios existen.
  • Escribir la comprobación que demuestra que un cuerpo malformado se rechaza sin tocar el estado.

1Por qué importa

La validación de entrada es la frontera de confianza de una API. Todo lo que la cruza sin comprobarse se convierte en un supuesto que la lógica de negocio da por cierto: que monto es un número, que usuario es una cadena, que no llegan campos que nadie esperaba. Cada uno de esos supuestos, si no se verifica en la puerta, es una vía para que un cuerpo inesperado provoque un comportamiento inesperado: un error 500 que filtra una traza, una operación que se ejecuta con un tipo que rompe una comprobación posterior, o un campo de más que acaba en la base de datos.

La salida importa por la razón simétrica. Una API que devuelve el registro entero "porque es cómodo" filtra los campos internos que ese registro arrastra: el hash de la contraseña, el identificador interno, marcas de estado que revelan cómo funciona el sistema por dentro. El cliente no pidió esos campos y no debería verlos, pero ahí están, en cada respuesta, esperando a que alguien los lea. Y la enumeración es la más sutil de las tres: no filtra un dato directamente, filtra la existencia de un dato, que muchas veces es justo lo que el atacante necesita para el siguiente paso.

El objetivo real

No estás aprendiendo a poner if not campo: error por todas partes. Estás aprendiendo a tratar el cuerpo de una petición como entrada hostil hasta que un esquema demuestre lo contrario, y a tratar cada respuesta como una filtración potencial hasta que una lista de campos públicos demuestre que solo sale lo que debe.

2Conceptos

Vocabulario mínimo
  • Validación de sintaxis: ¿el cuerpo es JSON bien formado? Si no, 400 y punto.
  • Validación de esquema: ¿tiene los campos correctos, del tipo correcto, y ninguno de más?
  • Esquema: la descripción declarativa de la forma válida de un cuerpo (campos, tipos, rangos, obligatoriedad).
  • Sobre-exposición de salida: devolver campos que el cliente no necesita y no debería ver (clave, id interno).
  • Lista de campos públicos: la allow-list aplicada a la respuesta; solo esos campos salen.
  • Enumeración: deducir qué valores existen (usuarios, ids) a partir de las diferencias en las respuestas.
  • Oráculo: cualquier señal —código, mensaje, tiempo— que responde "sí/no" a una pregunta que no debería responder.

3Explicación profunda

Conviene separar dos cosas que se llaman igual. Validar la sintaxis es comprobar que el cuerpo es JSON bien formado; un cuerpo truncado o con una coma de más se rechaza con 400 antes de mirar nada. Validar el esquema es lo que viene después: el JSON parsea, pero ¿tiene los campos que este endpoint necesita, del tipo correcto, sin campos desconocidos? La mayoría de las APIs hacen lo primero de gratis, porque el parser falla solo, y se saltan lo segundo, que es justo donde vive la seguridad. Un cuerpo {"monto": "mucho"} es JSON perfectamente válido y a la vez completamente inválido para una transferencia.

Un esquema se declara, no se programa a mano. En vez de una cascada de if, se describe la forma válida —monto es un número positivo, usuario es una cadena de 3 a 32 caracteres, no se admiten campos adicionales— y una librería la aplica. La ventaja no es solo escribir menos: es que la validación es completa por defecto. Una cascada de if olvida casos; un esquema con additionalProperties: false rechaza cualquier campo que no haya declarado, incluido el que un atacante inventará mañana. Es la misma idea que la lista de campos permitidos de la lección anterior, ahora sobre la forma entera del cuerpo.

La validación de entrada tiene que ocurrir en la puerta, antes de la lógica de negocio, y tiene que ser fallo seguro: si algo no encaja, se rechaza y no se toca el estado. El error clásico es validar a medias y avanzar: comprobar que usuario existe pero no su tipo, empezar a operar, y fallar a la mitad dejando el sistema en un estado intermedio. Una petición inválida no debe producir ningún efecto lateral. O pasa entera la validación, o no pasa nada.

La salida es la otra mitad. Devolver el objeto de dominio directamente es la causa más común de fuga de datos por API: ese objeto arrastra campos que existen por razones internas y que nunca debieron viajar al cliente. La defensa es un modelo de salida explícito —un DTO de respuesta, una serialización con lista de campos públicos— de modo que añadir un campo interno al modelo de dominio no lo publique por accidente. Igual que en la entrada, lo seguro es enumerar lo que sale, no lo que se oculta.

La enumeración es más fina porque no filtra el dato, filtra su existencia. Si /login responde 404 "usuario no existe" y 401 "clave incorrecta", un atacante prueba nombres y, sin acertar ni una contraseña, obtiene la lista exacta de usuarios reales. Lo mismo con /perfil/<usuario> que devuelve 200 o 404, o con un registro que responde distinto según si el correo ya está dado de alta. La existencia de una cuenta es un dato, y a menudo el que abre el siguiente ataque: relleno de credenciales dirigido, phishing a usuarios confirmados, o simplemente saber a quién atacar. La defensa es hacer las respuestas indistinguibles: el mismo código, el mismo mensaje, y —cuando importa— el mismo tiempo de respuesta, porque un login que tarda más cuando el usuario existe es un oráculo temporal.

4Ejemplo sencillo

# El portero de un edificio

VALIDACION DE SINTAXIS
    ¿traes un papel? si no, no pasas. (¿es JSON?)

VALIDACION DE ESQUEMA
    ¿el papel es el formulario correcto, relleno, sin
    hojas de mas pegadas detras? (¿tiene la forma valida?)

# El portero que enumera sin querer

Preguntas por "Ana Torres, 4o B".
  - "aqui no vive nadie con ese nombre"   -> no existe
  - "vive aqui pero no te dejo subir"     -> existe

Sin subir a ningun piso, ya sabes quien vive en el
edificio: el portero te lo confirmo con dos respuestas
distintas. La respuesta correcta es la misma para los
dos casos: "no puedo darte esa informacion".
El portero honesto valida el papel entero antes de dejar pasar, y da la misma respuesta exista o no la persona. Las dos cosas son la misma disciplina: no confiar en la entrada y no filtrar por la salida.

5Ejemplo técnico

El laboratorio deja ver los tres fallos con tres peticiones. Míralos como un mapa de lo que la API acepta de más y devuelve de más:

PeticiónQué pasa en la API vulnerableQué debería pasar
POST /registro {"usuario":42,"clave":[]}Lo acepta: no comprueba tipos400 por esquema: usuario debe ser cadena
GET /perfil/anaDevuelve el registro entero, clave incluidaSolo campos públicos: usuario y rol
GET /perfil/zzz vs /perfil/ana404 vs 200: confirma quién existeRespuesta uniforme para el cliente que no es dueño

Escrito como riesgo con actor, vía, activo y consecuencia: "Cualquiera puede enumerar la lista completa de usuarios reales comparando los códigos de respuesta de /perfil y /login, y leer la clave de cualquier usuario en la salida de /perfil, lo que convierte una API de consulta en un directorio de credenciales."

6Diagrama

Una petición pasa por dos puertas: validación de esquema en la entrada y filtro de campos públicos en la salida. Entre ambas, la lógica de negocio solo ve datos con la forma correcta y solo puede devolver campos públicos. petición cuerpo hostil validar esquema tipos, campos, forma lógica datos con forma válida filtrar salida solo campos públicos 400 uniforme sin clave ni id interno si no encaja
La lógica de negocio vive entre dos puertas. Nunca ve un cuerpo con la forma equivocada, y nunca puede devolver un campo que no esté en la lista de públicos.
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
    P[petición · cuerpo hostil] --> V[validar esquema]
    V -->|no encaja| E[400 uniforme]
    V -->|encaja| L[lógica]
    L --> S[filtrar salida · solo campos públicos]
    S --> O[respuesta sin clave ni id interno]

7Código

El registro del laboratorio no valida tipos y devuelve el registro entero. La validación de esquema y el filtro de salida se pueden escribir a mano y quedan claros:

# Validación de esquema en la puerta: tipos, obligatoriedad, nada de más
PUBLICOS = ("usuario", "rol")   # lo unico que puede SALIR

def validar_registro(cuerpo):
    if not isinstance(cuerpo, dict):
        return "el cuerpo debe ser un objeto"
    permitidos = {"usuario", "clave"}
    extra = set(cuerpo) - permitidos
    if extra:
        return "campos no permitidos"          # additionalProperties: false
    u, c = cuerpo.get("usuario"), cuerpo.get("clave")
    if not isinstance(u, str) or not (3 <= len(u) <= 32):
        return "usuario invalido"
    if not isinstance(c, str) or len(c) < 8:
        return "clave invalida"
    return None                            # None = valido

def solo_publicos(registro):
    return {k: registro[k] for k in PUBLICOS if k in registro}

Y la corrección de la enumeración: un cliente que no es dueño del perfil recibe siempre la misma respuesta, exista el usuario o no. Solo el dueño (probado por el token) obtiene el 200:

# Respuesta uniforme: no confirmar la existencia a quien no es dueño
def perfil(nombre, quien_llama):
    if nombre != quien_llama:
        # mismo codigo y mensaje exista o no: sin oraculo
        return 404, {"error": "no encontrado"}
    registro = USUARIOS.get(nombre)
    if registro is None:
        return 404, {"error": "no encontrado"}
    return 200, solo_publicos(registro)   # nunca la clave

La clave ya no aparece en ninguna respuesta, y "no existe" y "no es tuyo" dan la misma salida. Un atacante que pruebe nombres no aprende nada.

8Qué puede salir mal

Los errores clásicos

Validar sintaxis y creer que es validación. Que el cuerpo sea JSON válido no dice nada sobre su forma. {"monto": "mucho"} parsea y es basura para la lógica.

Olvidar additionalProperties: false. Un esquema que valida los campos que conoce pero ignora los que no, deja pasar el campo de más. La validación tiene que rechazar lo desconocido, no solo comprobar lo conocido.

Serializar el objeto de dominio. Devolver el modelo tal cual filtra sus campos internos. El día que alguien añade un campo notas_internas al modelo, aparece en todas las respuestas sin que nadie lo decidiera.

Mensajes de error demasiado útiles. "El usuario no existe" y "clave incorrecta" son dos mensajes que juntos forman un oráculo de enumeración. Uno solo, uniforme, cierra la puerta: "usuario o clave incorrectos".

9Cómo lo abusaría un atacante

Vista desde el otro lado

La enumeración es reconocimiento, y el reconocimiento es barato. Un atacante escribe un bucle que prueba mil nombres de usuario contra /login con una clave cualquiera y anota cuáles devuelven 401 (existe) y cuáles 404 (no existe). En unos segundos tiene la lista exacta de cuentas reales, sin haber acertado ni una contraseña. Esa lista no es el premio: es el mapa para el siguiente ataque, que ahora se dirige solo a usuarios que sabe que existen.

Los tipos inesperados son la otra palanca. Enviar un array donde se espera una cadena, un número gigantesco donde se espera un entero pequeño, o un objeto anidado donde se espera un valor plano, busca provocar un error 500. Ese error muchas veces trae una traza que revela el framework, la versión, rutas del sistema de ficheros o fragmentos de la consulta. La validación de esquema convierte todos esos casos en un 400 aburrido y sin información.

Y la sobre-exposición de salida se explota sin esfuerzo: se lee. Cada campo que la API devuelve de más es información gratis sobre cómo funciona por dentro. Un id interno secuencial revela cuántos registros hay y permite iterarlos; un campo de estado revela la máquina de estados del negocio; una clave, evidentemente, lo revela todo. El atacante no tiene que hacer nada especial: la API se lo cuenta en cada respuesta.

10Cómo defenderlo

Qué hacer con esto mañana
  • Declara un esquema por endpoint y valídalo en la puerta, antes de la lógica. Con additionalProperties: false para rechazar lo desconocido.
  • Trata una petición inválida como sin efecto: o pasa entera la validación, o no se toca el estado.
  • Define un modelo de salida con la lista de campos públicos. Nunca serialices el objeto de dominio directamente.
  • Uniforma las respuestas de autenticación: un solo mensaje para "usuario o clave incorrectos", el mismo código, y cuidado con el tiempo.
  • No confirmes la existencia de un recurso a quien no está autorizado a saberlo: mismo 404 para "no existe" y "no es tuyo".
  • Convierte los errores 500 en 400 validados: un cuerpo raro nunca debe llegar a producir una traza para el cliente.

11Laboratorio

Estado: laboratorio local lab-api-vuln
Targethttp://localhost:8096
Autorizaciónsistema local creado por ti para entrenamiento
Esta API tiene fallos a propósito y es tuya. Todo lo que se practica aquí se practica aquí dentro. Ninguna de estas técnicas debe usarse contra una API que no sea tuya o para la que no tengas autorización escrita.

Levanta la API y observa los tres fallos: entrada sin esquema, salida de más y enumeración.

cd cursos/seguridad/labs/lab-api-vuln docker compose up -d --build
atacar → observar → defender → verificar
curl -s -X POST http://localhost:8096/registro \ -H 'Content-Type: application/json' -d '{"usuario":42,"clave":[]}' # lo acepta: no valida tipos (usuario deberia ser cadena) curl -s http://localhost:8096/perfil/ana { "usuario": "ana", "clave": "sol-de-verano-19", "rol": "cliente", ... } # la salida trae la clave: sobre-exposicion curl -s -o /dev/null -w "ana=%{http_code} " http://localhost:8096/perfil/ana curl -s -o /dev/null -w "zzz=%{http_code}\n" http://localhost:8096/perfil/zzz ana=200 zzz=404 # el codigo confirma quien existe: enumeracion

Ahora defiende las tres. En api/servidor.py: añade una validación de esquema a _registro (tipos y additionalProperties), filtra la salida de /perfil a los campos públicos, y uniforma /login a un solo mensaje. Reconstruye y comprueba:

docker compose up -d --build curl -s http://localhost:8096/perfil/ana

La respuesta ya no debe traer clave, el cuerpo con tipos malos debe dar 400, y /login debe responder igual para usuario inexistente y clave incorrecta. Cuando termines:

docker compose down -v

12Ejercicios

Sintaxis, esquema o salida

Clasifica cada fallo como validación de sintaxis, de esquema o de salida: (1) la API acepta {"monto": "mucho"}; (2) la API devuelve el hash de la contraseña; (3) la API revienta con un cuerpo JSON truncado; (4) la API acepta un campo es_admin que no esperaba; (5) la respuesta trae un id interno secuencial.

Sintaxis: ¿es JSON? Esquema: ¿tiene la forma correcta? Salida: ¿devuelve de más?
Aceptar un campo no esperado es un fallo de esquema (falta additionalProperties: false).
Los casos (2) y (5) no son sobre lo que entra, sino sobre lo que sale.
Solución

(1) esquema: tipo incorrecto. (2) salida: sobre-exposición. (3) sintaxis: JSON malformado. (4) esquema: campo no permitido. (5) salida: filtra un dato interno.

Clave: esquema · salida · sintaxis · esquema · salida. Solo (3) lo detecta el parser gratis; el resto necesita esquema o filtro de salida explícitos.

Filtra la salida de /perfil

Con el lab-api-vuln levantado, modifica el handler de /perfil en api/servidor.py para que devuelva solo los campos públicos (usuario y rol) y nunca la clave. Reconstruye y demuestra con curl que la clave ya no aparece en la respuesta.

El fallo está en que do_GET devuelve el registro completo.
Construye un diccionario nuevo con solo las claves públicas antes de responder.
Verifica con curl -s http://localhost:8096/perfil/ana | grep clave: no debe encontrar nada.
Solución

Aplica la función solo_publicos de la lección antes de responder, con PUBLICOS = ("usuario", "rol"). La verificación:

# tras reconstruir, esto no debe imprimir nada
curl -s http://localhost:8096/perfil/ana | grep -i clave \
  && echo "FALLA: aun filtra la clave" || echo "OK: sin clave"

Clave: el éxito es que la respuesta solo trae usuario y rol. Serializar el registro entero es la causa; el filtro explícito de campos públicos es la cura.

El oráculo temporal

Uniformar el mensaje de /login no cierra del todo la enumeración si el tiempo de respuesta delata al usuario. Investiga por qué un login puede tardar más cuando el usuario existe (pista: la verificación de la contraseña) y diseña cómo hacer que el tiempo sea indistinguible. ¿Qué se hace cuando el usuario no existe para igualar el coste?

Verificar una contraseña con una función de derivación es caro a propósito; no verificar nada es instantáneo.
Busca "user enumeration timing attack" y "dummy password hash".
Si el usuario no existe, se puede verificar la clave contra un hash señuelo para gastar el mismo tiempo.
Solución

El login existente hace más trabajo cuando el usuario existe: recupera su hash y ejecuta la función de derivación, que es cara por diseño. Cuando el usuario no existe, sale al instante. Esa diferencia de tiempo es un oráculo aunque el mensaje sea idéntico. La defensa es hacer siempre el mismo trabajo: si el usuario no existe, verificar la clave recibida contra un hash señuelo fijo, de modo que el coste temporal sea igual en ambos casos. Así el atacante no puede distinguir por reloj lo que ya no distingue por mensaje.

Clave: igualar el trabajo, no solo el mensaje. Verificar contra un hash señuelo cuando el usuario no existe hace el tiempo indistinguible y cierra el oráculo temporal.

Red Team · enumera todos los usuarios

Dentro del lab-api-vuln, y solo dentro de él: sin acertar ninguna contraseña, obtén la lista de usuarios que existen usando solo las diferencias de respuesta de /login o /perfil. Escribe el bucle y explica qué señal usaste para decidir "existe" o "no existe".

Prueba una clave cualquiera para varios nombres y mira solo el código de estado.
401 y 404 significan cosas distintas en esta API; esa diferencia es el oráculo.
Un bucle en sh con una lista de nombres y curl -o /dev/null -w "%{http_code}" basta.
Solución

Un bucle prueba nombres contra /login con una clave falsa: 401 significa "existe pero clave mal", 404 significa "no existe". También sirve GET /usuarios, que en esta API regala la lista entera. La señal es la diferencia de código; no hizo falta acertar ninguna contraseña. En una API bien hecha, los dos casos darían la misma respuesta y este bucle no aprendería nada.

# dentro del cliente del lab
for n in ana beto zzz tesoreria nadie; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST http://api:8000/login -H 'Content-Type: application/json' \
    -d "{\"usuario\":\"$n\",\"clave\":\"x\"}")
  echo "$n -> $code"
done

Clave: 401 = existe, 404 = no existe. La enumeración no necesita adivinar contraseñas; le basta con que la API responda distinto. La defensa es una respuesta uniforme.

Blue Team · detecta la enumeración en los logs

Ejecuta el bucle de enumeración del ejercicio anterior y mira docker compose logs api. ¿Qué patrón deja un ataque de enumeración en el log de acceso, y qué regla lo detectaría sin dar falsos positivos con un usuario que se equivoca de contraseña un par de veces?

La enumeración genera muchas peticiones al mismo endpoint desde el mismo origen en poco tiempo.
Un humano falla la clave 2 o 3 veces; un bucle prueba cientos de nombres distintos.
La señal no es "muchos 401", es "muchos usuarios distintos probados desde un origen en una ventana corta".
Solución

En el log aparece una ráfaga de POST /login desde el mismo origen, con muchos nombres de usuario distintos, en pocos segundos. Lo que distingue enumeración de un usuario torpe no es el número de errores, sino la variedad de usuarios probados: un humano insiste en su propia cuenta, un atacante recorre una lista. Una regla razonable cuenta usuarios distintos por origen y ventana de tiempo, y alerta por encima de un umbral. Para que la regla exista, el log tiene que registrar el usuario probado (no la clave) además del origen y la hora.

Clave: la señal es la diversidad de usuarios por origen y ventana, no el volumen de 401. El log debe traer usuario probado, origen y hora; nunca la clave.

Architecture Challenge · esquema como contrato

Diseña cómo la validación de entrada y salida se vuelve parte del contrato de la API y no una comprobación dispersa. Define: dónde vive el esquema, cómo se comparte entre la validación en tiempo de ejecución y la documentación, cómo evitas que un endpoint nuevo se despliegue sin esquema, y cómo garantizas que ninguna respuesta serialice el objeto de dominio directamente.

Un esquema declarativo (OpenAPI, JSON Schema) puede alimentar a la vez la validación y la documentación.
Si la validación se genera del mismo esquema que la documentación, no se desincronizan.
Un test de contrato puede recorrer todos los endpoints y fallar si alguno no declara esquema de entrada y de salida.
Solución

Una propuesta razonable: un esquema declarativo por endpoint (OpenAPI o JSON Schema) que es la única fuente de verdad; de él se genera tanto la validación en tiempo de ejecución como la documentación, de modo que no puedan divergir. La salida se define con su propio esquema de campos públicos, y la serialización pasa siempre por él —nunca por el objeto de dominio— de forma que un campo nuevo del dominio no se publique sin declararse. En el pipeline, una comprobación que falle si un endpoint no tiene esquema de entrada y de salida, y un test de contrato que envíe tipos inválidos a cada endpoint y exija un 400.

Clave: no hay una única respuesta. Se evalúa que el esquema sea una sola fuente para validación y documentación, que la salida pase por un modelo de campos públicos, y que haya una puerta que impida desplegar un endpoint sin esquema.

13Preguntas de comprensión

Comprobación

¿Qué añade la validación de esquema sobre "la app ya comprueba algunas cosas"?

Un esquema con additionalProperties: false es completo por defecto: rechaza tipos malos y campos desconocidos, incluido el que un atacante invente mañana.

La API responde 404 si el usuario no existe y 401 si la clave es incorrecta. ¿Qué regala?

Dos respuestas distintas dejan probar nombres y separar los que existen de los que no, sin acertar ninguna clave. La defensa es una respuesta uniforme.

¿Por qué es un fallo que /perfil devuelva el registro entero?

Serializar el objeto de dominio publica sus campos internos. La cura es un modelo de salida con solo los campos públicos.

Validar la entrada, ¿de qué otra clase de problema protege además del dato mal formado?

Un tipo inesperado busca provocar un 500 que filtra información del sistema. La validación de esquema convierte todos esos casos en un 400 sin datos.

0 / 4

14Reto adicional

El registro también enumera

La enumeración no vive solo en el login. Piensa en el registro: si POST /registro responde 409 "ese usuario ya existe", acabas de dar un oráculo para enumerar cuentas por la puerta de alta. Diseña cómo un flujo de registro puede evitar confirmar la existencia de una cuenta y aun así impedir duplicados. ¿Qué papel juega el correo de confirmación aquí?

Responder distinto según si el usuario ya existe es el mismo oráculo que en el login.
Si el registro siempre responde "te hemos enviado un correo", no confirma nada al que solo prueba nombres.
La unicidad se puede comprobar sin decírselo al cliente: el correo de confirmación lleva la información al dueño real, no al atacante.
Solución

El registro responde siempre igual —"si el correo es válido, te hemos enviado un enlace de confirmación"— exista o no ya la cuenta. Si existe, el correo real recibe un aviso de "alguien intentó registrarse con tu dirección"; si no existe, recibe el enlace de alta. El atacante que prueba direcciones no aprende nada por la respuesta de la API: la información va al buzón del dueño, no a quien pregunta. La unicidad se garantiza del lado del servidor sin filtrarla por la respuesta síncrona.

Clave: mover la señal de existencia fuera de la respuesta inmediata y hacia el canal del dueño (el correo) cierra el oráculo sin perder la garantía de unicidad. Responder distinto según exista la cuenta es el error.

15Resumen

Puntos clave
  • Validar sintaxis (¿es JSON?) no es validar esquema (¿tiene la forma correcta?). La seguridad vive en lo segundo.
  • Un esquema con additionalProperties: false es completo por defecto: rechaza tipos malos y campos desconocidos.
  • La validación ocurre en la puerta y es fallo seguro: una petición inválida no toca el estado.
  • La salida se filtra a una lista de campos públicos; nunca se serializa el objeto de dominio.
  • La enumeración filtra la existencia de un dato mediante diferencias de código, mensaje o tiempo.
  • La defensa contra enumeración es hacer las respuestas indistinguibles, incluido el tiempo.
¿Qué diferencia hay entre validar sintaxis y validar esquema?
Sintaxis: ¿el cuerpo es JSON bien formado? Esquema: ¿tiene los campos correctos, del tipo correcto y ninguno de más? El parser da la primera gratis; la segunda hay que declararla.
¿Por qué no se debe serializar el objeto de dominio en la respuesta?
Porque arrastra campos internos (clave, id, estado) que el cliente no debería ver. Un modelo de salida con campos públicos evita publicarlos por accidente.
¿Qué es un oráculo de enumeración?
Cualquier señal —código, mensaje o tiempo— que responde "existe/no existe" a quien no debería saberlo. Se cierra uniformando la respuesta.
¿Por qué el mensaje uniforme de login puede no bastar?
Porque el tiempo puede delatar: verificar la contraseña de un usuario que existe es caro. Se iguala verificando contra un hash señuelo cuando el usuario no existe.

16Mis notas

Cierre de la lección

Estas notas se guardan solo en este navegador. No se envían a ningún servidor. Expórtalas desde el panel de progreso si cambias de máquina.

17Recursos

Los comentarios se cargan solo si los pides, desde GitHub Discussions. No se carga nada de terceros mientras no pulses.