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

  1. Cómo se lee código con ojos de revisor
  2. Familia 1: inventar criptografía
  3. Familia 2: aleatoriedad rota
  4. Familia 3: cifrado simétrico mal usado
  5. Familia 4: hashes y contraseñas
  6. Familia 5: asimétrica mal aplicada
  7. Familia 6: TLS y certificados
  8. Familia 7: secretos y tokens
  9. La tabla-índice: todos los _MAL del curso
  10. La checklist del revisor de MediNube
  11. 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/codificar usadas 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 dragging lo rompen con papel y lápiz. La ofuscación cae con un strings o 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 random cerca 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 de time.time() o del PID.
  • Por qué es explotable: el Mersenne Twister de random es 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 con token_recuperacion_MAL (01-03): MalloryClinic podía regenerar el token de recuperación de ana.perez sabiendo 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.urandom para 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 / sha1 para 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) y login_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 el verify de 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) firmaba json.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 formato receta-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 (o CURLOPT_SSL_VERIFYPEER = 0, o un TrustManager vacío en Java): el clásico de los clásicos, nuestro llamar_api_MAL/estado_receta_MAL (05-02). El canal queda cifrado con quien sea — MITM trivial. Suele nacer como "fix" temporal de un SSLError y fosilizarse. Arreglo: arreglar la causa (CA interna en verify="/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_API de 01-04 y el privkey.pem commiteado 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; registrar jti/sub, nunca el token.
  • JWT sin validar alg/exp/aud/iss: el verificar_MAL de 06-03; alg: none y 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

  1. ¿Todo lo secreto/impredecible sale de secrets/os.urandom (nunca random, uuid, tiempo)?
  2. ¿Los secretos nuevos vienen del gestor (nada hardcodeado, nada en .env commiteado)?
  3. ¿Ningún secreto puede acabar en logs, mensajes de error o URLs?
  4. ¿Los tokens de un solo uso se almacenan hasheados y caducan?

Cifrado

  1. ¿Todo cifrado simétrico es AEAD (GCM/ChaCha20-Poly1305) — nada de ECB/CBC/CTR a pelo?
  2. ¿El nonce es aleatorio, de 12 bytes, único por operación y viaja junto al cifrado?
  3. ¿El AAD fija el contexto (id de paciente, versión de formato)?
  4. ¿El formato lleva byte de versión (criptoagilidad) y hay plan de rotación de clave?
  5. Si hay contraseñas como origen de claves: ¿KDF con coste (scrypt/Argon2), no un hash?

Hashes, MACs y contraseñas

  1. ¿Nada de MD5/SHA-1 en contextos de seguridad?
  2. ¿Contraseñas solo con Argon2id (y pepper del gestor)? ¿check_needs_rehash en el login?
  3. ¿Autenticación de mensajes con HMAC (jamás sha256(clave+msg)) y comparación con compare_digest?
  4. ¿Los webhooks llevan timestamp y se rechaza fuera de la ventana anti-replay?

Asimétrica y firmas

  1. ¿RSA solo con OAEP (cifrado) / PSS (firmas), tamaños ≥ 3072; o directamente Ed25519/X25519?
  2. ¿Todo lo que se firma se canonicaliza antes? ¿Los intercambios de claves están autenticados?

TLS y certificados

  1. ¿Cero verify=False (ni equivalentes) en todo el diff, tests incluidos?
  2. ¿Conexiones internas (BD incluida) con verificación completa (sslmode=verify-full + CA interna)?
  3. ¿Los certificados nuevos entran en la renovación automática y en el monitor de caducidades?

Tokens y sesiones

  1. ¿Todo jwt.decode fija algorithms (lista cerrada), audience, issuer y require? ¿exp corto?
  2. ¿El token de sesión va en cookie HttpOnly; Secure; SameSite (no localStorage)?

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=False de llamar_api_MAL nació 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. bandit y los linters detectan md5, random en contexto de seguridad y verify=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.

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

  1. 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.randint para 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, formato versió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.

  2. 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) Sin X-MediNube-Timestamp ni ventana anti-replay: un webhook capturado se reinyecta indefinidamente (03-02, checklist 13). Y el logging.info con la firma completa roza la familia 7: las firmas válidas en logs facilitan el replay si falta (e).

  3. Cuatro errores (y medio). (a) verify_signature: False: el token no se verifica en absoluto — cualquiera fabrica {"rol": "farmacia"} con crear_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 comprobar exp ni aud, tokens eternos y reutilizables entre servicios. (c) verify=False en requests: 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.

© Copyright 2026. Todos los derechos reservados