En la lección anterior aseguramos quién puede acceder a cada dato. Ahora protegemos el dato en sí. A02 – Fallos Criptográficos (Cryptographic Failures) agrupa todo lo que sale mal cuando la información sensible no se cifra, se cifra con algoritmos débiles, o se gestiona mal la protección de claves y secretos. En el Top Ten 2021 esta categoría absorbió la antigua "Exposición de Datos Sensibles" de 2017: el foco pasó de la consecuencia (el dato expuesto) a la causa (el fallo criptográfico que lo permitió).
El impacto es directo sobre la confidencialidad: contraseñas, datos personales, números de tarjeta (PAN) o tokens que acaban en manos equivocadas por viajar en claro, guardarse sin cifrar o protegerse con algoritmos rotos. En BazarNube encontraremos dos joyas típicas del código heredado: contraseñas guardadas con un hash obsoleto y números de tarjeta almacenados "por comodidad". Vamos a arreglarlas.
Aviso legal y ético: los ejemplos usan datos ficticios. No proceses ni almacenes datos reales de tarjetas fuera de un entorno certificado, y prueba solo sobre sistemas propios o con autorización explícita.
Contenido
- Identificar datos sensibles y su ciclo de vida
- Protección en tránsito: TLS bien configurado
- Protección en reposo: cifrado y gestión de claves
- Hashing de contraseñas: bcrypt/argon2 frente a MD5/SHA1
- Datos que directamente no deberías almacenar (PAN, CVV)
- Gestión de secretos y claves
- Errores comunes y consejos
- Ejercicios y soluciones
- Identificar datos sensibles y su ciclo de vida
Antes de cifrar hay que saber qué proteger. En BazarNube clasificamos:
| Dato | Sensibilidad | En tránsito | En reposo |
|---|---|---|---|
| Contraseña | Crítica | TLS | Hash lento con sal (nunca cifrado reversible) |
| Datos personales (nombre, dirección) | Alta | TLS | Cifrado en BD |
| PAN (número de tarjeta) | Crítica (PCI-DSS) | TLS | Preferible no almacenar; si es obligatorio, tokenizar/cifrar |
| CVV | Crítica | TLS | Prohibido almacenarlo, ni cifrado |
| Token de sesión | Alta | TLS + cookie segura | No persistir en claro |
La regla de partida: minimiza. El dato más seguro es el que no guardas.
- Protección en tránsito: TLS
Todo el tráfico de BazarNube (front React, API Express, módulo Java) debe ir sobre HTTPS/TLS. Errores frecuentes:
- Endpoints internos "solo HTTP" porque "están en la red privada" (un atacante que entra en la red los escucha).
- TLS mal configurado: versiones obsoletas (SSLv3, TLS 1.0/1.1), cifras débiles.
- Falta de HSTS, que fuerza al navegador a usar siempre HTTPS.
// app.js — forzar HTTPS y activar HSTS en Express
const helmet = require('helmet');
app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true, preload: true }));
// Redirigir HTTP -> HTTPS (detras de un proxy/terminador TLS)
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] === 'http') {
return res.redirect(301, 'https://' + req.headers.host + req.url);
}
next();
});HSTS con maxAge de un año e includeSubDomains evita ataques de degradación (SSL stripping). Veremos las cabeceras de seguridad con más detalle en A05 (03-06); aquí nos interesa la parte criptográfica: cifrar el canal siempre.
- Protección en reposo: cifrado y gestión de claves
Los datos personales de los clientes se cifran en PostgreSQL. Distingue:
- Cifrado simétrico (AES-256-GCM) para datos que necesitas recuperar en claro (p. ej. una dirección de envío).
- Hashing (irreversible) para lo que solo necesitas comparar (contraseñas). Nunca cifres una contraseña de forma reversible.
// crypto/fieldCrypto.js — cifrado autenticado de campos con AES-256-GCM
const crypto = require('crypto');
const KEY = Buffer.from(process.env.FIELD_KEY, 'hex'); // 32 bytes, desde el gestor de secretos
function encrypt(plaintext) {
const iv = crypto.randomBytes(12); // IV unico por operacion
const cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv);
const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag(); // integridad (GCM)
return Buffer.concat([iv, tag, enc]).toString('base64');
}
function decrypt(blob) {
const raw = Buffer.from(blob, 'base64');
const iv = raw.subarray(0, 12), tag = raw.subarray(12, 28), data = raw.subarray(28);
const decipher = crypto.createDecipheriv('aes-256-gcm', KEY, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
}Puntos clave: AES-GCM aporta cifrado y autenticidad (detecta manipulación); el IV es aleatorio y único por operación; la clave vive en un gestor de secretos, nunca en el código.
- Hashing de contraseñas: el caso de BazarNube
Aquí está el clásico del código legacy. El registro de usuarios guardaba así las contraseñas:
// auth.js — VULNERABLE: hash rapido, sin sal, roto
const crypto = require('crypto');
function storePassword(user, password) {
const hash = crypto.createHash('md5').update(password).digest('hex');
return db.query('UPDATE users SET pwd = $1 WHERE id = $2', [hash, user.id]);
}Por qué esto es un desastre
- MD5/SHA1 están rotos para este uso: son rápidos y hay colisiones conocidas.
- Sin sal: dos usuarios con la misma contraseña tienen el mismo hash; una rainbow table los revienta al instante.
- Rápidos = malos para contraseñas: un atacante con una GPU prueba miles de millones de MD5 por segundo. Si roba la tabla
users, recupera la mayoría de contraseñas en horas.
Explotación ilustrativa
Con la BD filtrada, el atacante toma el hash 5f4dcc3b5aa765d61d8327deb882cf99, lo busca en cualquier base de datos pública de hashes MD5 y obtiene al instante la contraseña password. Sin sal ni coste, es casi instantáneo.
Código corregido con bcrypt
// auth.js — SEGURO: bcrypt con sal automatica y coste ajustable
const bcrypt = require('bcrypt');
const COST = 12; // factor de trabajo; subir con el tiempo
async function storePassword(user, password) {
const hash = await bcrypt.hash(password, COST); // genera e incluye la sal
return db.query('UPDATE users SET pwd = $1 WHERE id = $2', [hash, user.id]);
}
async function verifyPassword(password, storedHash) {
return bcrypt.compare(password, storedHash); // comparacion en tiempo constante
}bcrypt genera una sal aleatoria por contraseña (incluida en el propio hash) y es deliberadamente lento (el factor de coste 12 obliga a muchas iteraciones), lo que hace inviable el crackeo masivo. La comparación es en tiempo constante, evitando ataques de temporización.
Comparativa de algoritmos
| Algoritmo | ¿Apto para contraseñas? | Motivo |
|---|---|---|
| MD5 / SHA1 | No | Rotos, rápidos, sin sal |
| SHA-256 "a secas" | No | Demasiado rápido para contraseñas |
| bcrypt | Sí | Lento, con sal, muy probado |
| argon2id | Sí (preferido hoy) | Resistente a GPU/ASIC, coste en memoria |
| PBKDF2 | Aceptable | Válido con muchas iteraciones (uso legado/FIPS) |
Para un servicio nuevo, argon2id es la recomendación actual del OWASP Password Storage Cheat Sheet; bcrypt sigue siendo una opción sólida y ampliamente disponible.
Migración sin conocer las contraseñas
BazarNube no puede "descifrar" los MD5 antiguos para rehashear. La estrategia es rehashear en el próximo login válido: si el usuario acierta con el hash viejo, se recalcula con bcrypt y se marca migrado. Los que no vuelven, se fuerzan a resetear tras un plazo.
async function login(email, password) {
const u = await getUser(email);
if (u.pwd_alg === 'md5') {
if (md5(password) !== u.pwd) return fail();
await storePassword(u, password); // rehash con bcrypt
await setAlg(u, 'bcrypt');
return ok(u);
}
return (await verifyPassword(password, u.pwd)) ? ok(u) : fail();
}
- Datos que no deberías almacenar
El módulo de facturación Java guardaba el número de tarjeta completo (PAN) "para mostrar el historial". Esto entra en el ámbito de PCI-DSS y es un riesgo enorme.
- El CVV nunca puede almacenarse, ni cifrado, bajo ninguna circunstancia.
- El PAN solo debe almacenarse si es imprescindible, cifrado o tokenizado.
- Lo habitual es delegar el pago en un proveedor (PSP) que devuelve un token; BazarNube guarda solo los últimos 4 dígitos para mostrar, y nada más.
// PaymentRecord.java (modulo legacy) — VULNERABLE
record PaymentRecord(String pan, String cvv, BigDecimal amount) {} // guarda PAN y CVV// PaymentRecord.java — SEGURO: solo un token del PSP y los ultimos 4 digitos
record PaymentRecord(String pspToken, String last4, BigDecimal amount) {}
// El PAN completo y el CVV nunca tocan nuestra base de datos.Menos datos sensibles almacenados = menor superficie de exposición y menor alcance regulatorio.
- Gestión de secretos y claves
De nada sirve AES-256 si la clave está en el repositorio Git.
flowchart LR A[App BazarNube] -->|pide secreto en arranque| B[Gestor de secretos] B --> A C[Repositorio Git] -. nunca contiene secretos .-> A
Buenas prácticas:
- Secretos fuera del código: variables de entorno inyectadas o un gestor de secretos (Vault, AWS Secrets Manager, etc.). Nunca en Git, ni en
.envversionado. - Rotación de claves periódica y ante sospecha de compromiso.
- Separación de claves por entorno (dev/staging/prod) y por propósito.
- Si un secreto se filtra en un commit, rótalo: purgarlo del historial no basta, hay que asumirlo comprometido.
Errores Comunes y Consejos
- Cifrar contraseñas de forma reversible. Las contraseñas se hashean (bcrypt/argon2), no se cifran.
- Usar SHA-256 sin más para contraseñas. Es rápido; necesitas un algoritmo lento con sal.
- Guardar CVV o PAN completo sin necesidad. Delega en un PSP y guarda solo un token.
- HTTP en la red interna. El cifrado en tránsito aplica también dentro del perímetro.
- Claves en el repositorio. Usa un gestor de secretos; rota lo que se haya filtrado.
- Reutilizar el IV en AES. Cada operación necesita un IV único; con GCM, reutilizarlo rompe la seguridad.
- Consejo: clasifica los datos primero. No puedes proteger lo que no sabes que tienes.
Ejercicios
Ejercicio 1. Un compañero propone guardar contraseñas con SHA-256 + sal. ¿Resuelve el problema del MD5? ¿Qué recomendarías?
Ejercicio 2. Detecta los dos fallos criptográficos en este fragmento y corrígelos:
const key = 'clave-super-secreta-123';
const iv = Buffer.alloc(16, 0); // IV fijo de ceros
const cipher = crypto.createCipheriv('aes-256-cbc', key.padEnd(32,'0'), iv);Ejercicio 3. BazarNube necesita mostrar "tarjeta terminada en 4242" en el historial. ¿Qué debería almacenar exactamente y qué no?
Soluciones
Solución 1. Añadir sal a SHA-256 elimina las rainbow tables, pero SHA-256 sigue siendo demasiado rápido: un ataque de fuerza bruta con GPU sigue siendo viable. La recomendación es un algoritmo lento diseñado para contraseñas: argon2id (preferido) o bcrypt, que incorporan sal y coste ajustable.
Solución 2. Dos fallos: (1) el IV es fijo (todo ceros): rompe la confidencialidad al cifrar repetidamente; debe ser aleatorio por operación (crypto.randomBytes). (2) La clave es una cadena en el código, rellenada con ceros: debe ser una clave aleatoria de 32 bytes proveniente del gestor de secretos. Además, conviene AES-256-GCM (autenticado) en lugar de CBC. Ver la implementación de la sección 3.
Solución 3. Almacenar únicamente: un token del proveedor de pago (para recobros/referencia) y los últimos 4 dígitos (last4) más quizá la marca (Visa/Mastercard). Nunca el PAN completo ni el CVV.
Conclusión
A02 nos recuerda que proteger datos es un problema de causa (criptografía y gestión de claves), no solo de consecuencia. En el backlog de BazarNube apuntamos: TLS + HSTS en todo el tráfico, AES-256-GCM para datos personales en reposo, migración de contraseñas de MD5 a bcrypt/argon2 mediante rehash en login, eliminación del PAN/CVV a favor de tokens del PSP y traslado de todos los secretos a un gestor con rotación.
Entrada de backlog — A02: reemplazado el hashing MD5 por bcrypt (coste 12) con plan de migración; eliminado el almacenamiento de PAN/CVV en el módulo Java; cifrado de campos personales con AES-GCM; secretos movidos fuera de Git.
Hemos protegido el dato en tránsito y en reposo. Pero hay una familia de fallos donde el dato del usuario se cuela en un intérprete y toma el control: la inyección. Es la siguiente lección, A03:2021 – Inyección, con SQLi en la API Node/PostgreSQL de BazarNube y una inyección en el módulo legacy Java.
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
