Esta lección es distinta a todas las anteriores: no vas a aprender ninguna primitiva nueva, porque ya sabes todo lo necesario. Lo que vas a entrenar es la mirada de revisor: la capacidad de abrir un fichero ajeno (o tuyo de hace dos años) y que los errores criptográficos te salten a la vista antes de llegar a producción. Recorreremos una galería de errores organizada por familias —cada uno con su síntoma en el código, por qué es explotable, su arreglo y la lección donde lo aprendiste—, reuniremos por fin todos los _MAL del curso en una tabla-índice (como prometimos en 05-03), construiremos la checklist del revisor de MediNube y terminarás haciendo de auditor con tres fragmentos de código heredado que combinan varios pecados a la vez. Los ejercicios de hoy son la auditoría.
Contenido
- Cómo se lee código con ojos de revisor
- Familia 1: inventar criptografía
- Familia 2: aleatoriedad rota
- Familia 3: cifrado simétrico mal usado
- Familia 4: hashes y contraseñas
- Familia 5: asimétrica mal aplicada
- Familia 6: TLS y certificados
- Familia 7: secretos y tokens
- La tabla-índice: todos los
_MALdel curso - La checklist del revisor de MediNube
- Vocabulario compartido: OWASP y CWE
Cómo se lee código con ojos de revisor
Tres hábitos antes de la galería. Primero: los errores criptográficos casi nunca dan síntomas — el código roto cifra, descifra y pasa los tests; solo falla contra un adversario, y el adversario no está en CI. Segundo: los errores vienen en racimos — donde hay un md5, suele haber cerca un random y un ==; encontrar uno es la señal para frenar y leer todo el módulo. Tercero: revisa contra las 9 reglas de oro del módulo 1 — casi cada error de esta galería es una regla violada, y citarlas da a tu revisión un criterio que no suena a opinión personal.
Familia 1: inventar criptografía
Regla violada: la 1 (no inventes tu propia criptografía) y la 2 (el enemigo conoce el sistema).
- Síntoma en el código: bucles que hacen XOR de los datos con una clave que se repite; "cifrados" a base de sumar desplazamientos; funciones llamadas
ofuscar/codificarusadas como si protegieran; comentarios tipo "nadie sabrá que esto es Base64 al revés".
def cifrar_casero(datos: bytes, clave: bytes) -> bytes:
return bytes(d ^ clave[i % len(clave)] for i, d in enumerate(datos)) # XOR repetido- Por qué es explotable: el XOR con clave repetida es el cifrado de Vigenère con otra ropa: análisis de frecuencias y
crib dragginglo rompen con papel y lápiz. La ofuscación cae con unstringso un decompilador — Kerckhoffs (M1): asume que el atacante tiene el código. - Arreglo: AEAD de pyca/cryptography (AES-256-GCM o ChaCha20-Poly1305, M2). Sin excepciones ni "es solo para datos internos".
- Dónde se aprendió: módulo 1 completo; la tentación reaparecía en 02-01.
Familia 2: aleatoriedad rota
Regla violada: la 3 (toda aleatoriedad del CSPRNG) y la 5 (la entropía manda).
- Síntoma:
import randomcerca de la palabra "token", "clave", "nonce" o "reset";random.seed(...)con tiempo o con datos del usuario; UUIDs (uuid1/uuid4) usados como secretos; "aleatorio" derivado detime.time()o del PID. - Por qué es explotable: el Mersenne Twister de
randomes predecible — con 624 salidas se reconstruye el estado completo, y con semilla temporal ni eso hace falta: el atacante prueba las semillas del último minuto. Lo vimos contoken_recuperacion_MAL(01-03): MalloryClinic podía regenerar el token de recuperación deana.perezsabiendo la hora aproximada de la petición. Los UUID no prometen impredecibilidad (uuid1 lleva MAC y reloj; uuid4 depende de la implementación): son identificadores, no secretos. - Arreglo:
secrets.token_urlsafe(32)/secrets.token_bytes(32)/os.urandompara todo lo que deba ser secreto o impredecible; 128 bits mínimo, 256 estándar. - Dónde se aprendió: 01-03.
Familia 3: cifrado simétrico mal usado
Regla violada: la 7 (identifica el objetivo antes que la herramienta) — casi siempre por elegir modo sin pensar en integridad.
| Síntoma en el código | Por qué es explotable | Arreglo | Lección |
|---|---|---|---|
modes.ECB(...) |
Bloques iguales → cifrado igual: el pingüino de 02-02; estructuras y repeticiones visibles | AEAD (GCM/ChaCha20-Poly1305) | 02-02 |
Nonce/IV fijo (b"\x00"*12), contador reiniciado, o nonce derivado del propio dato |
En GCM/CTR, reutilizar nonce con la misma clave rompe la confidencialidad de ambos mensajes y permite falsificar tags | Nonce aleatorio de 12 bytes por operación, guardado junto al cifrado (formato v1 de 02-03) |
02-02/02-03 |
| CBC (o CTR) "a pelo", sin MAC — cifrar sin autenticar | Maleabilidad: el atacante voltea bits con efecto controlado; los padding oracle de CBC descifran sin la clave observando errores del servidor | AEAD siempre; si algo no es AEAD, HMAC en encrypt-then-MAC — pero en 2026, AEAD | 02-02/02-03 |
Clave derivada de una contraseña con un hash simple (sha256(password)) |
Fuerza bruta offline a velocidad de GPU | KDF con coste: scrypt/Argon2 (02-04); para derivar subclaves de una clave fuerte, HKDF | 02-04 |
| AAD vacío cuando hay contexto que fijar | El cifrado de un paciente puede "trasplantarse" a otro (confusión de contexto) | AAD con el contexto: paciente=...;formato=v1 |
02-03 |
El patrón mental de esta familia: confidencialidad sin integridad es una trampa. Si ves cifrado sin tag de autenticación, ya tienes el hallazgo.
Familia 4: hashes y contraseñas
Regla violada: la 6 (compara secretos en tiempo constante) y la 7.
- Síntomas y sus porqués:
hashlib.md5/sha1para cualquier cosa relacionada con seguridad: colisiones prácticas desde hace años (03-01); en firmas y certificados, falsificables.- Hash rápido para contraseñas (
sha256(password), con o sin salt): una GPU prueba miles de millones por segundo;guardar_password_MAL(03-03) caía en horas ante un diccionario. - Sin salt (o salt global): la misma contraseña produce el mismo hash → rainbow tables y ataque a todos los usuarios a la vez.
- Comparación no constante (
==,startswith) de hashes, tags o claves de API: el tiempo de respuesta filtra cuántos bytes coinciden —autenticar_MAL(01-04) ylogin_MAL(03-03). sha256(clave + mensaje)como MAC: length extension por la construcción Merkle–Damgård —tag_MAL(03-02): el atacante extiende el mensaje y calcula el tag válido sin conocer la clave.
- Arreglos: SHA-256/SHA-3/BLAKE2 para integridad; Argon2id con
PasswordHasher(y pepper HMAC del gestor, 06-01) para contraseñas;secrets.compare_digest(o elverifyde la librería) para comparar; HMAC para autenticar mensajes. - Dónde se aprendió: módulo 3 completo.
Familia 5: asimétrica mal aplicada
Regla violada: la 1 y la 7.
- RSA sin padding (libro de texto) o con PKCS#1 v1.5 donde no toca: RSA puro es determinista —
adivinar_resultado_MAL(04-01) mostraba cómo el atacante cifra los candidatos ("POSITIVO"/"NEGATIVO") con la pública y compara; v1.5 en cifrado arrastra los oracles de Bleichenbacher. Arreglo: OAEP para cifrar, PSS para firmar (04-01/04-03). - Claves cortas o curvas anticuadas: RSA-1024, P-192 (
generar_par_medico_MAL, 04-02). Arreglo: RSA-3072 mínimo, o mejor Ed25519/X25519. - DH/ECDH sin autenticar: intercambio X25519 sin verificar con quién intercambias = invitación al MITM de MalloryClinic (04-04). Arreglo: autenticar los extremos (firmas o certificados, como hace TLS).
- Firmar sin canonicalizar:
firmar_receta_MAL(04-03) firmabajson.dumps(receta)sin fijar orden ni separadores: el mismo contenido produce bytes distintos según el diccionario, y la verificación se vuelve una lotería (o algo peor: dos representaciones "válidas" del mismo documento). Arreglo: canonicalizar antes de firmar (sort_keys=True, separadores fijos — el formatoreceta-v1). - Cifrar datos grandes "directamente con RSA": además de imposible por tamaño, es el síntoma de no conocer el sobre híbrido. Arreglo: KEM/sobre — cifra una clave simétrica, no el dato (04-05).
Familia 6: TLS y certificados
Regla violada: la 9 (la criptografía no sustituye al resto de la seguridad) — de nada sirve TLS si le quitas la parte que autentica.
verify=False(oCURLOPT_SSL_VERIFYPEER = 0, o unTrustManagervacío en Java): el clásico de los clásicos, nuestrollamar_api_MAL/estado_receta_MAL(05-02). El canal queda cifrado con quien sea — MITM trivial. Suele nacer como "fix" temporal de unSSLErrory fosilizarse. Arreglo: arreglar la causa (CA interna enverify="/etc/medinube/pki/ca.crt", cadena completa en el servidor), jamás apagar la verificación.- Ignorar caducidades y revocación: certificados que caducan en viernes por la noche, CRL/OCSP sin consultar. Arreglo: renovación automática (ACME) + monitorización (
medinube/monitor_certs.py, UMBRAL_DIAS=30) — 05-03. - Versiones y suites antiguas: TLS 1.0/1.1, RC4, 3DES, suites sin PFS. Arreglo: TLS 1.3, mínimo 1.2 solo con ECDHE+AEAD (05-02).
- Pinning roto: fijar el certificado hoja "para más seguridad" y bloquear a tus propios usuarios en la primera rotación (peor con vidas cortas de ACME). Si se hace pinning, a la clave de la CA propia y con plan de rotación; para casi todos, CT + verificación estándar bastan.
Familia 7: secretos y tokens
Regla violada: la 2 y la 8.
- Hardcodear secretos: el
CLAVES_APIde 01-04 y elprivkey.pemcommiteado de 05-03. Arreglo: gestor de secretos, gitleaks en CI, rotación ante cualquier filtración (06-01). - Loguear secretos:
logger.info(f"token={token}"), contraseñas en el log de peticiones, el token JWT completo en los logs de acceso. Los logs viven años, se replican y los lee mucha gente. Arreglo: redactar en el logger; registrarjti/sub, nunca el token. - JWT sin validar
alg/exp/aud/iss: elverificar_MALde 06-03;alg: noney la confusión RS256→HS256 entran por aquí. Arreglo:algorithms=[...]cerrado,audience,issuer,require(06-03). - Tokens de vida eterna o refresh tokens guardados en claro en la BD. Arreglo: TTL corto + refresh rotatorio guardado como
sha256(token)(06-03, patrón de 03-03).
La tabla-índice: todos los _MAL del curso
Lo prometido en 05-03: la galería completa del código heredado de MediNube, reunida. Úsala como índice de repaso — si algún "pecado" no te resulta obvio al instante, esa lección merece una relectura:
| Función | Lección | Pecado | Arreglo |
|---|---|---|---|
crear_sesion_MAL / leer_sesion_MAL |
01-02 (saldada en 06-03) | Base64 como si fuera protección: token editable | JWT EdDSA con claims y verificación estricta |
token_recuperacion_MAL |
01-03 | random con semilla temporal para un token de reset |
secrets.token_urlsafe(32) + almacenar sha256(token) |
autenticar_MAL |
01-04 | Comparación == de claves de API (timing) + CLAVES_API hardcodeado |
secrets.compare_digest + gestor de secretos (06-01) |
enviar_webhook_MAL |
03-02 | "Integridad" con sha256(cuerpo) sin clave: cualquiera recalcula |
HMAC-SHA256 con clave por clínica |
tag_MAL |
03-02 | sha256(clave + mensaje) como MAC: length extension |
HMAC |
verificar_MAL (webhooks) |
03-02 | Lo anterior + comparación == + sin timestamp anti-replay |
HMAC + compare_digest + ventana de 300 s |
guardar_password_MAL / login_MAL |
03-03 | Hash rápido para contraseñas, comparación no constante | Argon2id (PasswordHasher) + pepper (06-01) |
adivinar_resultado_MAL |
04-01 | RSA sin padding: cifrado determinista, se adivina por diccionario | RSA-OAEP |
generar_par_medico_MAL |
04-02 | Curva P-192: seguridad efectiva por debajo del mínimo | Ed25519 (o P-256 si el ecosistema lo exige) |
firmar_receta_MAL |
04-03 | Firmar JSON sin canonicalizar | Canonicalización receta-v1 + Ed25519 |
llamar_api_MAL / estado_receta_MAL |
05-02 | verify=False: TLS sin autenticación = MITM servido |
Verificación con la CA correcta; arreglar la causa raíz |
verificar_MAL (JWT) |
06-03 | decode sin algorithms cerrado ni aud/iss/exp |
Lista cerrada + audience + issuer + require |
La checklist del revisor de MediNube
La lista que MediNube adjunta a toda revisión de código que toque seguridad. Agrupada para poder repartirla; cada punto es una pregunta con respuesta sí/no sobre el diff:
Aleatoriedad y secretos
- ¿Todo lo secreto/impredecible sale de
secrets/os.urandom(nuncarandom,uuid, tiempo)? - ¿Los secretos nuevos vienen del gestor (nada hardcodeado, nada en
.envcommiteado)? - ¿Ningún secreto puede acabar en logs, mensajes de error o URLs?
- ¿Los tokens de un solo uso se almacenan hasheados y caducan?
Cifrado
- ¿Todo cifrado simétrico es AEAD (GCM/ChaCha20-Poly1305) — nada de ECB/CBC/CTR a pelo?
- ¿El nonce es aleatorio, de 12 bytes, único por operación y viaja junto al cifrado?
- ¿El AAD fija el contexto (id de paciente, versión de formato)?
- ¿El formato lleva byte de versión (criptoagilidad) y hay plan de rotación de clave?
- Si hay contraseñas como origen de claves: ¿KDF con coste (scrypt/Argon2), no un hash?
Hashes, MACs y contraseñas
- ¿Nada de MD5/SHA-1 en contextos de seguridad?
- ¿Contraseñas solo con Argon2id (y pepper del gestor)? ¿
check_needs_rehashen el login? - ¿Autenticación de mensajes con HMAC (jamás
sha256(clave+msg)) y comparación concompare_digest? - ¿Los webhooks llevan timestamp y se rechaza fuera de la ventana anti-replay?
Asimétrica y firmas
- ¿RSA solo con OAEP (cifrado) / PSS (firmas), tamaños ≥ 3072; o directamente Ed25519/X25519?
- ¿Todo lo que se firma se canonicaliza antes? ¿Los intercambios de claves están autenticados?
TLS y certificados
- ¿Cero
verify=False(ni equivalentes) en todo el diff, tests incluidos? - ¿Conexiones internas (BD incluida) con verificación completa (
sslmode=verify-full+ CA interna)? - ¿Los certificados nuevos entran en la renovación automática y en el monitor de caducidades?
Tokens y sesiones
- ¿Todo
jwt.decodefijaalgorithms(lista cerrada),audience,issueryrequire? ¿expcorto? - ¿El token de sesión va en cookie
HttpOnly; Secure; SameSite(nolocalStorage)?
Vocabulario compartido: OWASP y CWE
Cuando un auditor externo revise MediNube, no dirá "familia 3": dirá OWASP Top 10 A02 – Cryptographic Failures (la categoría que agrupa casi toda esta lección) y citará CWEs concretos: CWE-327 (algoritmo roto o arriesgado), CWE-328 (hash débil), CWE-330 (aleatoriedad insuficiente), CWE-259/798 (credenciales hardcodeadas), CWE-295 (validación de certificado incorrecta), CWE-347 (verificación de firma incorrecta). Te conviene conocer estos códigos como vocabulario compartido — mapear tus hallazgos a A02/CWE hace tus informes legibles para seguridad y compliance. No lo desarrollamos más aquí: el portal tiene un curso específico de OWASP donde este marco se trabaja a fondo.
Errores Comunes y Consejos
- Error (del revisor): buscar solo el bug puntual. Los errores criptográficos van en racimos; cuando encuentres uno, revisa el módulo entero y el historial de git de ese fichero.
- Error: aceptar "es temporal" o "es solo interno". El
verify=Falsedellamar_api_MALnació como fix temporal en 2023. Los apaños criptográficos fosilizan: o entran bien, o no entran. - Error: señalar sin arreglo. Un hallazgo sin propuesta ("usa HMAC con la clave del gestor, ver 03-02") tiende a cerrarse como "won't fix". La tabla-índice te da el arreglo canónico de cada pecado.
- Consejo: automatiza lo automatizable.
bandity los linters detectanmd5,randomen contexto de seguridad yverify=False; gitleaks caza secretos. La checklist humana es para lo que las herramientas no ven (AAD ausente, canonicalización, diseño). - Consejo: revisa también los tests. Es donde se esconden los
verify=False, las claves de "prueba" que acaban en producción y los mocks que desactivan la verificación de firmas.
Ejercicios
Los tres fragmentos siguientes son "código heredado" real de MediNube, rescatado de ramas antiguas. Cada uno combina varios errores de familias distintas. Audítalos: lista cada error, por qué es explotable y su arreglo (cita la lección o el punto de la checklist). Cuenta antes de mirar las soluciones: hay entre 4 y 6 por fragmento.
- Exportación de historiales para una clínica:
import random, hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def exportar_historiales(historiales: bytes, password: str) -> bytes:
clave = hashlib.md5(password.encode()).digest() # 16 bytes, "suficiente"
iv = bytes([random.randint(0, 255) for _ in range(16)])
cifrador = Cipher(algorithms.AES(clave), modes.CBC(iv)).encryptor()
relleno = 16 - len(historiales) % 16
ct = cifrador.update(historiales + bytes([relleno]) * relleno) + cifrador.finalize()
return iv + ct- Receptor de webhooks de resultados de laboratorio:
import hashlib, logging
CLAVE_WEBHOOK = "clave-compartida-2023" # la misma para todas las clínicas
def recibir_webhook(cuerpo: bytes, cabeceras: dict) -> bool:
esperado = hashlib.sha256(CLAVE_WEBHOOK.encode() + cuerpo).hexdigest()
logging.info(f"Webhook recibido, firma={cabeceras['X-MediNube-Firma']}")
if cabeceras["X-MediNube-Firma"] == esperado:
procesar(cuerpo)
return True
return False- Cliente del servicio de recetas para la Farmacia Robles:
import requests, jwt
def consultar_receta(id_receta: str, token: str) -> dict:
datos = jwt.decode(token, options={"verify_signature": False}) # "ya viene de MediNube"
if datos["rol"] != "farmacia":
raise PermissionError
r = requests.get(f"https://api.medinube.example/recetas/{id_receta}",
headers={"Authorization": f"Bearer {token}"},
verify=False, timeout=5)
return r.json()Soluciones
-
Cinco errores. (a)
md5(password)como KDF: hash roto y sin coste — fuerza bruta offline a velocidad de GPU; arreglo: scrypt/Argon2 con salt (02-04, checklist 9/10). (b)random.randintpara el IV: predecible (familia 2); arreglo:os.urandom(16)— aunque con el arreglo (d) desaparece. (c) AES-128 por accidente (la clave es de 16 bytes porque MD5 da 16): decisión de tamaño tomada por un hash, no por diseño. (d) CBC con padding manual y sin autenticación: maleable y candidato a padding oracle en el import correspondiente (familia 3); arreglo: AES-256-GCM con AAD (clinica=...;formato=v1). (e) Sin versionado de formato: irrotable (checklist 8). Versión correcta: scrypt(password, salt) → AESGCM, formatoversión || salt || nonce || ct+tag— es exactamente el backup de 02-04. Bonus: para un destinatario externo, mejor el sobre híbrido de 04-05 y ninguna contraseña compartida. -
Cinco errores. (a) Clave hardcodeada en el código (familia 7 → gestor, 06-01). (b) La misma clave para todas las clínicas: Clínica Sol puede falsificar webhooks "de" Centro Médico Luna; arreglo: clave por clínica (03-02). (c)
sha256(clave + cuerpo): length extension (familia 4,tag_MAL); arreglo: HMAC-SHA256. (d) Comparación con==: timing (checklist 12); arreglo:hmac.compare_digest. (e) SinX-MediNube-Timestampni ventana anti-replay: un webhook capturado se reinyecta indefinidamente (03-02, checklist 13). Y ellogging.infocon la firma completa roza la familia 7: las firmas válidas en logs facilitan el replay si falta (e). -
Cuatro errores (y medio). (a)
verify_signature: False: el token no se verifica en absoluto — cualquiera fabrica{"rol": "farmacia"}concrear_sesion_MAL-style y pasa el control; arreglo: verificar con la pública de MediNube,algorithms=["EdDSA"],audience,issuer,require(06-03). (b) Aunque verificara: sin comprobarexpniaud, tokens eternos y reutilizables entre servicios. (c)verify=Falseenrequests: MITM sobre datos de recetas — y encima manda el Bearer token al posible impostor, regalando la credencial (05-02 + familia 7); arreglo: verificación TLS estándar (es un dominio público con CA pública; ni siquiera hay excusa de CA interna). (d) Autorización basada en un claim (rol) de un token no verificado = decisión de seguridad sobre datos del cliente, el pecado original de 01-02. El "medio":r.json()sin comprobar el código de estado — no es criptográfico, pero un revisor completo lo anota.
Conclusión
Ya tienes lo que ningún manual da por sí solo: criterio. Sabes que los errores criptográficos no avisan, que vienen en racimos, y tienes tres herramientas para cazarlos: las siete familias (con el porqué explotable de cada una), la tabla-índice de todos los _MAL que MediNube ha ido arreglando durante el curso, y una checklist de veinte puntos lista para usar en tu próxima revisión. La galería queda cerrada: del Base64 editable de 01-02 al verify=False de 2023, todo el código heredado tiene ya nombre, diagnóstico y arreglo.
Queda una última mirada, y no es hacia atrás sino hacia adelante: hay un byte reservado en el sobre híbrido de MediNube (0x03, desde 04-05) esperando a la criptografía que resistirá a los ordenadores cuánticos. Qué amenaza exactamente un ordenador cuántico, qué sobrevive de nuestro arsenal, qué son ML-KEM y ML-DSA y qué debe hacer MediNube hoy — eso es la última lección del curso. Nos vemos en el futuro.
Curso de Criptografía Aplicada
Módulo 1: Fundamentos de la Criptografía
- Qué es la criptografía y para qué sirve
- Codificación, ofuscación y cifrado
- Aleatoriedad y entropía
- El principio de Kerckhoffs y las reglas de oro
Módulo 2: Criptografía Simétrica
- Cifrado simétrico: AES y ChaCha20
- Modos de operación
- Cifrado autenticado (AEAD)
- Derivación de claves (KDF)
Módulo 3: Hashes, MAC y Contraseñas
- Funciones hash criptográficas
- Autenticación de mensajes con HMAC
- Almacenamiento seguro de contraseñas
Módulo 4: Criptografía Asimétrica
- Fundamentos de clave pública y RSA
- Criptografía de curva elíptica
- Firmas digitales
- Intercambio de claves: Diffie-Hellman
- Cifrado híbrido
Módulo 5: PKI, Certificados y TLS
- Certificados X.509 y autoridades de certificación
- TLS en la práctica
- Gestión del ciclo de vida de certificados
