Hasta ahora hemos corregido fallos de implementación: una consulta concatenada, una comprobación de autorización olvidada, un innerHTML peligroso. Pero hay una clase de vulnerabilidades que ninguna corrección de código elimina, porque el problema no está en cómo se escribió el código, sino en qué se decidió construir. Esa es A04:2021 – Diseño Inseguro (Insecure Design), una de las dos categorías nuevas del Top Ten 2021 (junto con A10 SSRF).
La idea central: puedes implementar un diseño inseguro de forma impecable —código limpio, sin SQLi, sin XSS— y aun así ser vulnerable, porque faltaban controles de seguridad en el propio diseño. En BazarNube lo veremos con el flujo de cupones de descuento y checkout: un diseño que no contempló los límites de negocio permite que un cliente combine descuentos hasta pagar cero euros. No hay ningún bug de código; el fallo es que nadie pensó en el abuso al diseñar el flujo.
Esta lección introduce el modelado de amenazas como herramienta para descubrir estos fallos temprano. Lo presentamos aquí de forma breve; su desarrollo completo está en el módulo 7 (lección 07-02).
Aviso legal y ético: los escenarios de abuso son ilustrativos y con datos ficticios. Practica solo sobre sistemas propios o con autorización explícita.
Contenido
- Diseño inseguro vs. implementación insegura
- El caso de los cupones de BazarNube
- Requisitos de seguridad y casos de abuso
- Límites de confianza y patrones de diseño seguro
- Introducción al modelado de amenazas
- Errores comunes, ejercicios y soluciones
- Diseño inseguro vs. implementación insegura
| Aspecto | Implementación insegura | Diseño inseguro |
|---|---|---|
| Origen | Se implementó mal un control que existía | Faltaba el control en el diseño |
| Ejemplo | Consulta SQL concatenada (A03) | No se definió límite de intentos ni de negocio |
| Se detecta con | Revisión de código, SAST/DAST | Modelado de amenazas, revisión de requisitos |
| Se corrige con | Parchear el código | Rediseñar el flujo / añadir controles |
Una pista para distinguirlos: si arreglas el fallo cambiando cómo está escrita una función, era de implementación; si necesitas añadir un control o regla de negocio que no existía, era de diseño. Muchos incidentes reales combinan ambos, pero A04 nos obliga a mirar antes del código: ¿el flujo, tal como fue concebido, resiste a un usuario malicioso?
- El caso de los cupones de BazarNube
El equipo de producto pidió cupones de descuento para captar clientes. Marc y Lucía implementaron el flujo tal como se especificó, y el código es correcto: valida el cupón, comprueba que existe, aplica el porcentaje. El endpoint de checkout:
// checkout.js — codigo CORRECTO pero DISENO inseguro
router.post('/api/checkout', requireLogin, async (req, res) => {
const cart = await getCart(req.session.userId);
let total = cartTotal(cart);
for (const code of req.body.coupons) { // acepta una LISTA de cupones
const c = await getCoupon(code);
if (c && c.active) total -= total * (c.percent / 100); // se aplican todos
}
const order = await createOrder(req.session.userId, cart, total);
res.json({ orderId: order.id, total });
});No hay SQLi, ni IDOR, ni XSS. El código hace exactamente lo que se diseñó. El problema es lo que no se diseñó:
- No se limitó el número de cupones por pedido (el flujo acepta una lista).
- No se comprobó si un cupón es acumulable con otros.
- No se validó un descuento máximo ni un importe mínimo a pagar.
- No se limitó el número de usos por cliente.
Cómo se explota (abuso de lógica)
Un cliente descubre tres cupones válidos del 40 %, 35 % y 30 %. Los envía todos en el mismo checkout:
Aplicados en cascada, el total cae casi a cero. Peor: como no hay límite de usos, automatiza pedidos y vacía el stock a coste simbólico. Ninguna herramienta de escaneo de código lo detecta, porque no hay ningún patrón inseguro: es una decisión de negocio que faltó.
Corrección: añadir los controles de diseño
La solución no es "parchear una línea", es incorporar los requisitos de seguridad de negocio que faltaban:
// checkout.js — DISENO seguro: reglas de negocio explicitas
router.post('/api/checkout', requireLogin, async (req, res) => {
const cart = await getCart(req.session.userId);
const subtotal = cartTotal(cart);
// Regla 1: un unico cupon por pedido (no acumulables por defecto)
const code = req.body.coupon;
let discount = 0;
if (code) {
const c = await getCoupon(code);
if (!c || !c.active) return res.status(400).json({ error: 'Cupon invalido' });
// Regla 2: limite de usos por cliente
if (await couponUsedBy(code, req.session.userId) >= c.maxPerUser)
return res.status(400).json({ error: 'Cupon ya utilizado' });
// Regla 3: importe minimo de compra
if (subtotal < c.minPurchase)
return res.status(400).json({ error: 'No alcanza el minimo' });
discount = subtotal * (c.percent / 100);
}
// Regla 4: descuento maximo global e importe minimo a pagar
discount = Math.min(discount, subtotal * 0.5);
const total = Math.max(subtotal - discount, 0.01);
const order = await createOrder(req.session.userId, cart, total, code);
res.json({ orderId: order.id, total });
});Fíjate en que la corrección son reglas de negocio de seguridad, no técnicas de codificación: un cupón por pedido, límite de usos, mínimo de compra, tope de descuento e importe mínimo a pagar. Estas reglas debieron surgir en la fase de diseño, no descubrirse tras el abuso.
- Requisitos de seguridad y casos de abuso
El origen del fallo fue trabajar solo con casos de uso ("el cliente aplica un cupón y paga menos") y olvidar los casos de abuso ("el cliente combina cupones para pagar cero"). Diseñar con seguridad implica escribir ambos.
| Caso de uso (usuario legítimo) | Caso de abuso (atacante) | Requisito de seguridad derivado |
|---|---|---|
| Aplica un cupón de bienvenida | Combina varios cupones | Máximo un cupón por pedido |
| Usa su cupón una vez | Reutiliza el cupón en bucle | Límite de usos por cliente |
| Compra con descuento | Fuerza total = 0 | Importe mínimo a pagar y tope de descuento |
| Se registra para recibir cupón | Crea cuentas masivas para farmear cupones | Verificación de email / límite por identidad |
De cada caso de abuso nace un requisito de seguridad que debe formar parte de la especificación, no ser un añadido posterior. Este es el corazón de A04: la seguridad como requisito de diseño.
- Límites de confianza y patrones de diseño seguro
Un límite de confianza (trust boundary) es toda frontera donde los datos pasan de una zona menos fiable a otra más fiable (del navegador al servidor, del servidor al módulo de pagos). En cada límite hay que validar y volver a decidir. El error de diseño frecuente es asumir que "si viene del paso anterior, es de fiar".
flowchart LR A[Cliente React] -->|limite de confianza| B[API Express] B -->|limite de confianza| C[Modulo pagos Java] C -->|limite de confianza| D[PSP externo]
Patrones de diseño seguro que BazarNube adopta:
- Validación en el servidor de las reglas de negocio, no solo en el cliente (el cliente es sugerencia, el servidor es autoridad).
- Límites y cuotas por diseño: rate limiting, máximos por operación, importes mínimos.
- Estados y transiciones explícitas: un pedido no puede pasar de "creado" a "enviado" sin pasar por "pagado". Diseñar la máquina de estados evita saltos abusivos.
- Fail-safe / denegar por defecto (lo vimos en A01): ante la duda, denegar.
- Reutilizar componentes seguros probados en lugar de reinventar (librerías de pago, de autenticación).
- Introducción al modelado de amenazas
El modelado de amenazas (threat modeling) es la práctica que habría evitado el fallo de los cupones: analizar el diseño antes de construir, preguntándose qué puede salir mal. De forma resumida, responde a cuatro preguntas (marco de Shostack):
- ¿Qué estamos construyendo? (diagrama del flujo, actores, límites de confianza)
- ¿Qué puede salir mal? (amenazas; un método común es STRIDE)
- ¿Qué vamos a hacer al respecto? (controles/mitigaciones)
- ¿Lo hicimos bien? (validación)
Aplicado a los cupones, la pregunta "¿qué puede salir mal?" habría revelado de inmediato "un cliente combina cupones" y "los usa en bucle", generando los requisitos que faltaban.
No desarrollamos aquí el método completo (STRIDE, diagramas de flujo de datos, priorización): eso corresponde al módulo 7, lección 07-02 (Modelado de Amenazas). Por ahora, quédate con la idea operativa: incorpora una sesión de modelado de amenazas al diseñar cada flujo sensible (pagos, cupones, registro, permisos), y convierte cada amenaza en un requisito de seguridad verificable.
Errores Comunes y Consejos
- Confundir "sin bugs" con "seguro". Un flujo puede no tener un solo bug de código y ser abusable por diseño.
- Diseñar solo casos de uso felices. Escribe también los casos de abuso; de ahí salen los requisitos.
- Validar reglas de negocio solo en el front. El cliente es manipulable; la autoridad es el servidor.
- Dejar la seguridad para "después". Rediseñar un flujo en producción es carísimo; modelar amenazas al principio es barato.
- No definir límites ni cuotas. Todo flujo económico o de recursos necesita topes explícitos.
- Consejo: para cada nueva funcionalidad sensible, dedica 30 minutos a preguntar "¿cómo abusaría yo de esto?" antes de implementarla.
Ejercicios
Ejercicio 1. BazarNube diseña un sistema de "puntos de fidelidad": el cliente gana puntos por compra y los canjea por saldo. Enumera tres casos de abuso y su requisito de seguridad derivado.
Ejercicio 2. ¿Cuál de estos fallos es de diseño y cuál de implementación? Justifica. a) El endpoint de cupones concatena el código en una consulta SQL. b) El sistema permite devolver un producto y quedarse con él porque no verifica que se haya recibido en almacén.
Ejercicio 3. El equipo propone "arreglar" el abuso de cupones validando en el front que solo se envíe un cupón. ¿Resuelve el problema? ¿Por qué?
Soluciones
Solución 1. Ejemplos: (a) Abuso: generar compras y devoluciones para farmear puntos → Requisito: solo acreditar puntos tras pedido completado y no reembolsado. (b) Abuso: canjear más puntos de los que se tienen por condición de carrera → Requisito: descuento atómico y transaccional del saldo de puntos. (c) Abuso: transferir puntos entre cuentas propias creadas en masa → Requisito: límite por identidad verificada / no transferibles.
Solución 2. a) Implementación: el control (parametrizar) existe como práctica; se implementó mal una consulta. b) Diseño: falta una regla/estado en el flujo de devoluciones (verificar recepción antes de reembolsar); no es un bug de codificación sino un control ausente.
Solución 3. No lo resuelve. La validación en el front es solo UX: un atacante llama directamente a POST /api/checkout con varios cupones saltándose el navegador. Los controles de negocio deben imponerse en el servidor (como en la sección 2). El front puede guiar, pero no proteger.
Conclusión
A04 amplía la mirada: la seguridad empieza en el diseño, no en el código. Los fallos de diseño inseguro no los caza un escáner; se previenen escribiendo casos de abuso, derivando requisitos de seguridad, respetando los límites de confianza y aplicando modelado de amenazas temprano. En BazarNube, rediseñar el flujo de cupones con límites de negocio explícitos cerró un agujero que ninguna corrección de línea habría tapado.
Entrada de backlog — A04: rediseñado el checkout de cupones con reglas de negocio (un cupón por pedido, límite de usos, mínimo de compra, tope de descuento, importe mínimo a pagar); adoptada una sesión de modelado de amenazas para flujos económicos; pendiente de aplicar el método completo en M7.
Del diseño pasamos ahora a cómo se despliega y configura lo construido. Aunque el código y el diseño sean correctos, una configuración descuidada abre la puerta. La siguiente lección es A05:2021 – Configuración de Seguridad Incorrecta, con la config de Express y Docker de BazarNube.
Curso de OWASP: Directrices y Estándares para la Seguridad en Aplicaciones Web
Módulo 1: Introducción a OWASP
Módulo 2: Principales Proyectos de OWASP
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Otros Proyectos Clave: WSTG, Cheat Sheets y Dependency-Check
Módulo 3: OWASP Top Ten 2021 en Profundidad
- A01:2021 – Pérdida de Control de Acceso
- A02:2021 – Fallos Criptográficos y Exposición de Datos Sensibles
- A03:2021 – Inyección
- Cross-Site Scripting (XSS) en Profundidad
- A04:2021 – Diseño Inseguro
- A05:2021 – Configuración de Seguridad Incorrecta
- Entidades Externas XML (XXE)
- A06:2021 – Componentes Vulnerables y Desactualizados
- A07:2021 – Fallos de Identificación y Autenticación
- A08:2021 – Fallos de Integridad de Software y Datos (Deserialización Insegura)
- A09:2021 – Fallos de Registro y Monitorización
- A10:2021 – Server-Side Request Forgery (SSRF)
Módulo 4: OWASP ASVS (Application Security Verification Standard)
- Introducción a ASVS
- Niveles de Verificación
- Requisitos de Seguridad
- Implementación de ASVS en Proyectos
Módulo 5: OWASP SAMM (Software Assurance Maturity Model)
Módulo 6: OWASP ZAP (Zed Attack Proxy)
- Introducción a ZAP
- Instalación y Configuración
- Escaneo de Vulnerabilidades
- Automatización de Pruebas de Seguridad
Módulo 7: Buenas Prácticas y Recomendaciones
- Ciclo de Vida de Desarrollo Seguro (SDLC)
- Modelado de Amenazas (Threat Modeling)
- Integración de Seguridad en DevOps (DevSecOps)
- Capacitación y Concienciación en Seguridad
- Herramientas y Recursos Adicionales
Módulo 8: Ejercicios Prácticos y Casos de Estudio
- Ejercicio 1: Identificación de Vulnerabilidades
- Ejercicio 2: Implementación de Controles de Seguridad
- Caso de Estudio 1: Análisis de un Incidente de Seguridad
- Caso de Estudio 2: Mejora de la Seguridad en una Aplicación Web
