Aquesta lliçó és diferent de totes les anteriors: no aprendràs cap primitiva nova, perquè ja saps tot el necessari. El que entrenaràs és la mirada de revisor: la capacitat d'obrir un fitxer aliè (o teu de fa dos anys) i que els errors criptogràfics et saltin a la vista abans d'arribar a producció. Recorrerem una galeria d'errors organitzada per famílies —cadascun amb el seu símptoma al codi, per què és explotable, el seu arranjament i la lliçó on el vas aprendre—, reunirem per fi tots els _MALAMENT del curs en una taula-índex (com vam prometre a 05-03), construirem la llista de verificació del revisor de MediNube i acabaràs fent d'auditor amb tres fragments de codi heretat que combinen diversos pecats alhora. Els exercicis d'avui són l'auditoria.

Contingut

  1. Com es llegeix codi amb ulls de revisor
  2. Família 1: inventar criptografia
  3. Família 2: aleatorietat trencada
  4. Família 3: xifratge simètric mal usat
  5. Família 4: hashes i contrasenyes
  6. Família 5: asimètrica mal aplicada
  7. Família 6: TLS i certificats
  8. Família 7: secrets i tokens
  9. La taula-índex: tots els _MALAMENT del curs
  10. La llista de verificació del revisor de MediNube
  11. Vocabulari compartit: OWASP i CWE

Com es llegeix codi amb ulls de revisor

Tres hàbits abans de la galeria. Primer: els errors criptogràfics gairebé mai no donen símptomes — el codi trencat xifra, desxifra i passa els tests; només falla contra un adversari, i l'adversari no és a CI. Segon: els errors vénen en raïms — on hi ha un md5, sol haver-hi a prop un random i un ==; trobar-ne un és el senyal per aturar-se i llegir tot el mòdul. Tercer: revisa contra les 9 regles d'or del mòdul 1 — gairebé cada error d'aquesta galeria és una regla violada, i citar-les dona a la teva revisió un criteri que no sona a opinió personal.

Família 1: inventar criptografia

Regla violada: l'1 (no inventis la teva pròpia criptografia) i la 2 (l'enemic coneix el sistema).

  • Símptoma al codi: bucles que fan XOR de les dades amb una clau que es repeteix; "xifratges" a base de sumar desplaçaments; funcions anomenades ofuscar/codificar usades com si protegissin; comentaris del tipus "ningú sabrà que això és Base64 al revés".
def xifrar_casola(dades: bytes, clau: bytes) -> bytes:
    return bytes(d ^ clau[i % len(clau)] for i, d in enumerate(dades))  # XOR repetit
  • Per què és explotable: el XOR amb clau repetida és el xifratge de Vigenère amb una altra roba: l'anàlisi de freqüències i el crib dragging el trenquen amb paper i llapis. L'ofuscació cau amb un strings o un descompilador — Kerckhoffs (M1): assumeix que l'atacant té el codi.
  • Arranjament: AEAD de pyca/cryptography (AES-256-GCM o ChaCha20-Poly1305, M2). Sense excepcions ni "és només per a dades internes".
  • On es va aprendre: mòdul 1 complet; la temptació reapareixia a 02-01.

Família 2: aleatorietat trencada

Regla violada: la 3 (tota aleatorietat del CSPRNG) i la 5 (l'entropia mana).

  • Símptoma: import random a prop de la paraula "token", "clau", "nonce" o "reset"; random.seed(...) amb el temps o amb dades de l'usuari; UUID (uuid1/uuid4) usats com a secrets; "aleatori" derivat de time.time() o del PID.
  • Per què és explotable: el Mersenne Twister de random és predictible — amb 624 sortides es reconstrueix l'estat complet, i amb llavor temporal ni això cal: l'atacant prova les llavors de l'últim minut. Ho vam veure amb token_recuperacio_MALAMENT (01-03): MalloryClinic podia regenerar el token de recuperació d'ana.perez sabent l'hora aproximada de la petició. Els UUID no prometen impredictibilitat (uuid1 porta MAC i rellotge; uuid4 depèn de la implementació): són identificadors, no secrets.
  • Arranjament: secrets.token_urlsafe(32) / secrets.token_bytes(32) / os.urandom per a tot el que hagi de ser secret o impredictible; 128 bits mínim, 256 estàndard.
  • On es va aprendre: 01-03.

Família 3: xifratge simètric mal usat

Regla violada: la 7 (identifica l'objectiu abans que l'eina) — gairebé sempre per triar el mode sense pensar en la integritat.

Símptoma al codi Per què és explotable Arranjament Lliçó
modes.ECB(...) Blocs iguals → xifrat igual: el pingüí de 02-02; estructures i repeticions visibles AEAD (GCM/ChaCha20-Poly1305) 02-02
Nonce/IV fix (b"\x00"*12), comptador reiniciat, o nonce derivat de la mateixa dada En GCM/CTR, reutilitzar nonce amb la mateixa clau trenca la confidencialitat de tots dos missatges i permet falsificar tags Nonce aleatori de 12 bytes per operació, desat junt al xifrat (format v1 de 02-03) 02-02/02-03
CBC (o CTR) "a pèl", sense MAC — xifrar sense autenticar Maleabilitat: l'atacant capgira bits amb efecte controlat; els padding oracle de CBC desxifren sense la clau observant errors del servidor AEAD sempre; si alguna cosa no és AEAD, HMAC en encrypt-then-MAC — però el 2026, AEAD 02-02/02-03
Clau derivada d'una contrasenya amb un hash simple (sha256(password)) Força bruta fora de línia a velocitat de GPU KDF amb cost: scrypt/Argon2 (02-04); per derivar subclaus d'una clau forta, HKDF 02-04
AAD buit quan hi ha context a fixar El xifrat d'un pacient es pot "transplantar" a un altre (confusió de context) AAD amb el context: pacient=...;format=v1 02-03

El patró mental d'aquesta família: confidencialitat sense integritat és un parany. Si veus xifrat sense tag d'autenticació, ja tens la troballa.

Família 4: hashes i contrasenyes

Regla violada: la 6 (compara secrets en temps constant) i la 7.

  • Símptomes i els seus perquès:
    • hashlib.md5 / sha1 per a qualsevol cosa relacionada amb seguretat: col·lisions pràctiques des de fa anys (03-01); en signatures i certificats, falsificables.
    • Hash ràpid per a contrasenyes (sha256(password), amb o sense sal): una GPU en prova milers de milions per segon; desar_contrasenya_MALAMENT (03-03) queia en hores davant un diccionari.
    • Sense sal (o sal global): la mateixa contrasenya produeix el mateix hash → rainbow tables i atac a tots els usuaris alhora.
    • Comparació no constant (==, startswith) de hashes, tags o claus d'API: el temps de resposta filtra quants bytes coincideixen — autenticar_MALAMENT (01-04) i iniciar_sessio_MALAMENT (03-03).
    • sha256(clau + missatge) com a MAC: length extension per la construcció Merkle–Damgård — tag_MALAMENT (03-02): l'atacant estén el missatge i calcula el tag vàlid sense conèixer la clau.
  • Arranjaments: SHA-256/SHA-3/BLAKE2 per a integritat; Argon2id amb PasswordHasher (i pepper HMAC del gestor, 06-01) per a contrasenyes; secrets.compare_digest (o el verify de la llibreria) per comparar; HMAC per autenticar missatges.
  • On es va aprendre: mòdul 3 complet.

Família 5: asimètrica mal aplicada

Regla violada: l'1 i la 7.

  • RSA sense farciment (de llibre de text) o amb PKCS#1 v1.5 on no toca: l'RSA pur és determinista — endevinar_resultat_MALAMENT (04-01) mostrava com l'atacant xifra els candidats ("POSITIU"/"NEGATIU") amb la pública i compara; v1.5 en xifratge arrossega els oracles de Bleichenbacher. Arranjament: OAEP per xifrar, PSS per signar (04-01/04-03).
  • Claus curtes o corbes antiquades: RSA-1024, P-192 (generar_parell_metge_MALAMENT, 04-02). Arranjament: RSA-3072 mínim, o millor Ed25519/X25519.
  • DH/ECDH sense autenticar: intercanvi X25519 sense verificar amb qui intercanvies = invitació al MITM de MalloryClinic (04-04). Arranjament: autenticar els extrems (signatures o certificats, com fa TLS).
  • Signar sense canonicalitzar: signar_recepta_MALAMENT (04-03) signava json.dumps(recepta) sense fixar ordre ni separadors: el mateix contingut produeix bytes diferents segons el diccionari, i la verificació es converteix en una loteria (o alguna cosa pitjor: dues representacions "vàlides" del mateix document). Arranjament: canonicalitzar abans de signar (sort_keys=True, separadors fixos — el format receta-v1).
  • Xifrar dades grans "directament amb RSA": a més d'impossible per mida, és el símptoma de no conèixer el sobre híbrid. Arranjament: KEM/sobre — xifra una clau simètrica, no la dada (04-05).

Família 6: TLS i certificats

Regla violada: la 9 (la criptografia no substitueix la resta de la seguretat) — de res serveix TLS si li treus la part que autentica.

  • verify=False (o CURLOPT_SSL_VERIFYPEER = 0, o un TrustManager buit en Java): el clàssic dels clàssics, el nostre cridar_api_MALAMENT/estat_recepta_MALAMENT (05-02). El canal queda xifrat amb qui sigui — MITM trivial. Sol néixer com a "fix" temporal d'un SSLError i fossilitzar-se. Arranjament: arreglar la causa (CA interna a verify="/etc/medinube/pki/ca.crt", cadena completa al servidor), mai apagar la verificació.
  • Ignorar caducitats i revocació: certificats que caduquen divendres a la nit, CRL/OCSP sense consultar. Arranjament: renovació automàtica (ACME) + monitorització (medinube/monitor_certs.py, LLINDAR_DIES=30) — 05-03.
  • Versions i suites antigues: TLS 1.0/1.1, RC4, 3DES, suites sense PFS. Arranjament: TLS 1.3, mínim 1.2 només amb ECDHE+AEAD (05-02).
  • Pinning trencat: fixar el certificat fulla "per a més seguretat" i bloquejar els teus propis usuaris a la primera rotació (pitjor amb vides curtes d'ACME). Si es fa pinning, a la clau de la CA pròpia i amb pla de rotació; per a gairebé tothom, CT + verificació estàndard basten.

Família 7: secrets i tokens

Regla violada: la 2 i la 8.

  • Hardcodejar secrets: el CLAUS_API de 01-04 i el privkey.pem commitejat de 05-03. Arranjament: gestor de secrets, gitleaks a CI, rotació davant qualsevol filtració (06-01).
  • Loguejar secrets: logger.info(f"token={token}"), contrasenyes al log de peticions, el token JWT complet als logs d'accés. Els logs viuen anys, es repliquen i molta gent els llegeix. Arranjament: redactar al logger; registrar jti/sub, mai el token.
  • JWT sense validar alg/exp/aud/iss: el verificar_MALAMENT de 06-03; alg: none i la confusió RS256→HS256 entren per aquí. Arranjament: algorithms=[...] tancat, audience, issuer, require (06-03).
  • Tokens de vida eterna o refresh tokens desats en clar a la BD. Arranjament: TTL curt + refresh rotatori desat com a sha256(token) (06-03, patró de 03-03).

La taula-índex: tots els _MALAMENT del curs

El que vam prometre a 05-03: la galeria completa del codi heretat de MediNube, reunida. Fes-la servir com a índex de repàs — si algun "pecat" no et resulta obvi a l'instant, aquesta lliçó mereix una relectura:

Funció Lliçó Pecat Arranjament
crear_sessio_MALAMENT / llegir_sessio_MALAMENT 01-02 (saldada a 06-03) Base64 com si fos protecció: token editable JWT EdDSA amb claims i verificació estricta
token_recuperacio_MALAMENT 01-03 random amb llavor temporal per a un token de reset secrets.token_urlsafe(32) + desar sha256(token)
autenticar_MALAMENT 01-04 Comparació == de claus d'API (timing) + CLAUS_API hardcodejat secrets.compare_digest + gestor de secrets (06-01)
enviar_webhook_MALAMENT 03-02 "Integritat" amb sha256(cos) sense clau: qualsevol la recalcula HMAC-SHA256 amb clau per clínica
tag_MALAMENT 03-02 sha256(clau + missatge) com a MAC: length extension HMAC
verificar_MALAMENT (webhooks) 03-02 L'anterior + comparació == + sense timestamp anti-replay HMAC + compare_digest + finestra de 300 s
desar_contrasenya_MALAMENT / iniciar_sessio_MALAMENT 03-03 Hash ràpid per a contrasenyes, comparació no constant Argon2id (PasswordHasher) + pepper (06-01)
endevinar_resultat_MALAMENT 04-01 RSA sense farciment: xifrat determinista, s'endevina per diccionari RSA-OAEP
generar_parell_metge_MALAMENT 04-02 Corba P-192: seguretat efectiva per sota del mínim Ed25519 (o P-256 si l'ecosistema ho exigeix)
signar_recepta_MALAMENT 04-03 Signar JSON sense canonicalitzar Canonicalització receta-v1 + Ed25519
cridar_api_MALAMENT / estat_recepta_MALAMENT 05-02 verify=False: TLS sense autenticació = MITM servit Verificació amb la CA correcta; arreglar la causa arrel
verificar_MALAMENT (JWT) 06-03 decode sense algorithms tancat ni aud/iss/exp Llista tancada + audience + issuer + require

La llista de verificació del revisor de MediNube

La llista que MediNube adjunta a tota revisió de codi que toqui seguretat. Agrupada per poder-la repartir; cada punt és una pregunta amb resposta sí/no sobre el diff:

Aleatorietat i secrets

  1. Tot el secret/impredictible surt de secrets/os.urandom (mai random, uuid, temps)?
  2. Els secrets nous vénen del gestor (res hardcodejat, res en .env commitejat)?
  3. Cap secret pot acabar en logs, missatges d'error o URL?
  4. Els tokens d'un sol ús es desen amb hash i caduquen?

Xifratge

  1. Tot xifratge simètric és AEAD (GCM/ChaCha20-Poly1305) — res d'ECB/CBC/CTR a pèl?
  2. El nonce és aleatori, de 12 bytes, únic per operació i viatja junt al xifrat?
  3. L'AAD fixa el context (id de pacient, versió de format)?
  4. El format porta byte de versió (criptoagilitat) i hi ha pla de rotació de clau?
  5. Si hi ha contrasenyes com a origen de claus: KDF amb cost (scrypt/Argon2), no un hash?

Hashes, MAC i contrasenyes

  1. Res de MD5/SHA-1 en contextos de seguretat?
  2. Contrasenyes només amb Argon2id (i pepper del gestor)? check_needs_rehash al login?
  3. Autenticació de missatges amb HMAC (mai sha256(clau+msg)) i comparació amb compare_digest?
  4. Els webhooks porten timestamp i es rebutja fora de la finestra anti-replay?

Asimètrica i signatures

  1. RSA només amb OAEP (xifratge) / PSS (signatures), mides ≥ 3072; o directament Ed25519/X25519?
  2. Tot el que se signa es canonicalitza abans? Els intercanvis de claus estan autenticats?

TLS i certificats

  1. Zero verify=False (ni equivalents) a tot el diff, tests inclosos?
  2. Connexions internes (BD inclosa) amb verificació completa (sslmode=verify-full + CA interna)?
  3. Els certificats nous entren a la renovació automàtica i al monitor de caducitats?

Tokens i sessions

  1. Tot jwt.decode fixa algorithms (llista tancada), audience, issuer i require? exp curt?
  2. El token de sessió va en cookie HttpOnly; Secure; SameSite (no localStorage)?

Vocabulari compartit: OWASP i CWE

Quan un auditor extern revisi MediNube, no dirà "família 3": dirà OWASP Top 10 A02 – Cryptographic Failures (la categoria que agrupa gairebé tota aquesta lliçó) i citarà CWE concrets: CWE-327 (algorisme trencat o arriscat), CWE-328 (hash feble), CWE-330 (aleatorietat insuficient), CWE-259/798 (credencials hardcodejades), CWE-295 (validació de certificat incorrecta), CWE-347 (verificació de signatura incorrecta). Et convé conèixer aquests codis com a vocabulari compartit — mapejar les teves troballes a A02/CWE fa que els teus informes siguin llegibles per a seguretat i compliment. No ho desenvolupem més aquí: el portal té un curs específic d'OWASP on aquest marc es treballa a fons.

Errors Comuns i Consells

  • Error (del revisor): buscar només el bug puntual. Els errors criptogràfics van en raïms; quan en trobis un, revisa el mòdul sencer i l'historial de git d'aquest fitxer.
  • Error: acceptar "és temporal" o "és només intern". El verify=False de cridar_api_MALAMENT va néixer com a fix temporal el 2023. Els apanys criptogràfics fossilitzen: o entren bé, o no entren.
  • Error: assenyalar sense arranjament. Una troballa sense proposta ("fes servir HMAC amb la clau del gestor, veure 03-02") tendeix a tancar-se com a "won't fix". La taula-índex et dona l'arranjament canònic de cada pecat.
  • Consell: automatitza el que sigui automatitzable. bandit i els linters detecten md5, random en context de seguretat i verify=False; gitleaks caça secrets. La llista de verificació humana és per al que les eines no veuen (AAD absent, canonicalització, disseny).
  • Consell: revisa també els tests. És on s'amaguen els verify=False, les claus de "prova" que acaben en producció i els mocks que desactiven la verificació de signatures.

Exercicis

Els tres fragments següents són "codi heretat" real de MediNube, rescatat de branques antigues. Cadascun combina diversos errors de famílies diferents. Audita'ls: llista cada error, per què és explotable i el seu arranjament (cita la lliçó o el punt de la llista de verificació). Compta abans de mirar les solucions: n'hi ha entre 4 i 6 per fragment.

  1. Exportació d'historials per a una clínica:
import random, hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

def exportar_historials(historials: bytes, contrasenya: str) -> bytes:
    clau = hashlib.md5(contrasenya.encode()).digest()          # 16 bytes, "suficient"
    iv = bytes([random.randint(0, 255) for _ in range(16)])
    xifrador = Cipher(algorithms.AES(clau), modes.CBC(iv)).encryptor()
    farciment = 16 - len(historials) % 16
    ct = xifrador.update(historials + bytes([farciment]) * farciment) + xifrador.finalize()
    return iv + ct
  1. Receptor de webhooks de resultats de laboratori:
import hashlib, logging

CLAU_WEBHOOK = "clau-compartida-2023"   # la mateixa per a totes les cliniques

def rebre_webhook(cos: bytes, capcaleres: dict) -> bool:
    esperat = hashlib.sha256(CLAU_WEBHOOK.encode() + cos).hexdigest()
    logging.info(f"Webhook rebut, firma={capcaleres['X-MediNube-Firma']}")
    if capcaleres["X-MediNube-Firma"] == esperat:
        processar(cos)
        return True
    return False
  1. Client del servei de receptes per a la Farmàcia Robles:
import requests, jwt

def consultar_recepta(id_recepta: str, token: str) -> dict:
    dades = jwt.decode(token, options={"verify_signature": False})  # "ja ve de MediNube"
    if dades["rol"] != "farmacia":
        raise PermissionError
    r = requests.get(f"https://api.medinube.example/receptes/{id_recepta}",
                     headers={"Authorization": f"Bearer {token}"},
                     verify=False, timeout=5)
    return r.json()

Solucions

  1. Cinc errors. (a) md5(contrasenya) com a KDF: hash trencat i sense cost — força bruta fora de línia a velocitat de GPU; arranjament: scrypt/Argon2 amb sal (02-04, llista de verificació 9/10). (b) random.randint per a l'IV: predictible (família 2); arranjament: os.urandom(16) — encara que amb l'arranjament (d) desapareix. (c) AES-128 per accident (la clau és de 16 bytes perquè MD5 dona 16): decisió de mida presa per un hash, no per disseny. (d) CBC amb farciment manual i sense autenticació: maleable i candidat a padding oracle en la importació corresponent (família 3); arranjament: AES-256-GCM amb AAD (clinica=...;format=v1). (e) Sense versionat de format: irrotable (llista de verificació 8). Versió correcta: scrypt(contrasenya, sal) → AESGCM, format versio || sal || nonce || ct+tag — és exactament la còpia de seguretat de 02-04. Bonus: per a un destinatari extern, millor el sobre híbrid de 04-05 i cap contrasenya compartida.

  2. Cinc errors. (a) Clau hardcodejada al codi (família 7 → gestor, 06-01). (b) La mateixa clau per a totes les clíniques: Clínica Sol pot falsificar webhooks "de" Centre Mèdic Luna; arranjament: clau per clínica (03-02). (c) sha256(clau + cos): length extension (família 4, tag_MALAMENT); arranjament: HMAC-SHA256. (d) Comparació amb ==: timing (llista de verificació 12); arranjament: hmac.compare_digest. (e) Sense X-MediNube-Timestamp ni finestra anti-replay: un webhook capturat es reinjecta indefinidament (03-02, llista de verificació 13). I el logging.info amb la signatura completa freguen la família 7: les signatures vàlides als logs faciliten el replay si en falta (e).

  3. Quatre errors (i mig). (a) verify_signature: False: el token no es verifica en absolut — qualsevol fabrica {"rol": "farmacia"} a l'estil crear_sessio_MALAMENT i passa el control; arranjament: verificar amb la pública de MediNube, algorithms=["EdDSA"], audience, issuer, require (06-03). (b) Encara que verifiqués: sense comprovar exp ni aud, tokens eterns i reutilitzables entre serveis. (c) verify=False a requests: MITM sobre dades de receptes — i a més envia el Bearer token al possible impostor, regalant la credencial (05-02 + família 7); arranjament: verificació TLS estàndard (és un domini públic amb CA pública; ni tan sols hi ha excusa de CA interna). (d) Autorització basada en un claim (rol) d'un token no verificat = decisió de seguretat sobre dades del client, el pecat original de 01-02. El "mig": r.json() sense comprovar el codi d'estat — no és criptogràfic, però un revisor complet ho anota.

Conclusió

Ja tens el que cap manual dona per si sol: criteri. Saps que els errors criptogràfics no avisen, que vénen en raïms, i tens tres eines per caçar-los: les set famílies (amb el perquè explotable de cadascuna), la taula-índex de tots els _MALAMENT que MediNube ha anat arreglant durant el curs, i una llista de verificació de vint punts a punt per fer servir en la teva propera revisió. La galeria queda tancada: del Base64 editable de 01-02 al verify=False de 2023, tot el codi heretat té ja nom, diagnòstic i arranjament.

Queda una última mirada, i no és cap enrere sinó cap endavant: hi ha un byte reservat al sobre híbrid de MediNube (0x03, des de 04-05) esperant la criptografia que resistirà els ordinadors quàntics. Què amenaça exactament un ordinador quàntic, què sobreviu del nostre arsenal, què són ML-KEM i ML-DSA i què ha de fer MediNube avui — això és l'última lliçó del curs. Ens veiem al futur.

© Copyright 2026. Tots els drets reservats