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

  1. Identificar datos sensibles y su ciclo de vida
  2. Protección en tránsito: TLS bien configurado
  3. Protección en reposo: cifrado y gestión de claves
  4. Hashing de contraseñas: bcrypt/argon2 frente a MD5/SHA1
  5. Datos que directamente no deberías almacenar (PAN, CVV)
  6. Gestión de secretos y claves
  7. Errores comunes y consejos
  8. Ejercicios y soluciones

  1. 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.

  1. 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.

  1. 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.

  1. 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 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();
}

  1. 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.

  1. 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 .env versionado.
  • 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

Módulo 3: OWASP Top Ten 2021 en Profundidad

Módulo 4: OWASP ASVS (Application Security Verification Standard)

Módulo 5: OWASP SAMM (Software Assurance Maturity Model)

Módulo 6: OWASP ZAP (Zed Attack Proxy)

Módulo 7: Buenas Prácticas y Recomendaciones

Módulo 8: Ejercicios Prácticos y Casos de Estudio

Módulo 9: Evaluación y Certificación

© Copyright 2026. Todos los derechos reservados