Commits
Auditoria pròpia de la plataforma: el bloc de seguretat, tancat
Revisió completa del projecte: 40 feines al TASKS.md, el P0 de seguretat
Per on ha començat
Al matí vaig tancar una feina arrossegada: l'onboarding per a col·laboradors externs. Els specs (46 fitxers) eren la referència de fase i vivien fora del repo; ara són a specs/, amb un docs/ONBOARDING.md que va de clonar el repo a tenir l'admin a dins, i una carpeta .agents/ amb comandes i trampes per a agents d'altres ordinadors.
Després vaig fer la revisió completa que duia setmanes esperant. En va sortir un docs/TASKS.md amb 40 feines ordenades per blocs: P0 seguretat, P1 integritat de dades, P2 accessibilitat, P3 rendiment. Allò comprovat en directe contra el servidor de dev va amb ⚠; allò només sospitat, amb ?.
El forat més gros: un JWT que ningú verificava
Quatre endpoints resolien l'usuari descodificant a mà el segon segment del token, sense comprovar la firma ni l'expiració. En la pràctica, això:
GET /api/publicitat/estadistiques sense cookie -> 401
GET /api/publicitat/estadistiques cookie falsa -> 200
La cookie falsa era literalment payload-token=<base64 de {"id":2}>.inventat. Ara tota la resolució d'usuari passa per payload.auth() des de src/lib/session.ts, un únic punt. De passada van caure dos forats més: qualsevol usuari es podia promoure a admin amb un PATCH a la seva pròpia fila (el guard només cobria el create), i Media era l'única col·lecció sense access d'escriptura declarat.
El simulacre que va destapar un restore trencat
El backup és íntegre; la ruta de restauració, no. Vaig restaurar el dump del dia en una base de proves i les taules coincidien amb producció, però restore.sh tenia tres problemes encadenats:
- Buscava el backup al lloc equivocat: llegia el sistema vell de ~/backups/, aturat des del 9 de setembre, i no el diari del contenidor.
- El dump el genera pg_dump 18 contra un Postgres 16.13 i porta SET transaction_timeout, que a la 16 no existeix. Amb ON_ERROR_STOP=1 avortava a la primera línia i deixava 0 taules; sense ell "funcionava" per accident.
- La sortida anava a /dev/null i no comptava res: un restore buit acabava dient OK.
Ara filtra els SET incompatibles, activa ON_ERROR_STOP i compta taules en acabar: menys de 10, exit 1. Verificat: amb filtre, 57 taules i agenda 1450; sense, zero.
La resta de la llista
- El rate limit de LaIA i de subscripcions era esquivable: quatre rutes llegien el PRIMER valor de X-Forwarded-For, el que controla el visitant. Ara es pren l'últim, el que afegeix el proxy, i el limitador viu tot a src/lib/ip.ts.
- Els beacons de publicitat anaven sense signar: demanar /ads/imp/ en bucle inflava les estadístiques i podia esgotar una campanya de pagament des de fora. El banner ara s'emet amb nonce + HMAC.
- SendGrid s'activava per la sola presència de la clau, i en dev n'hi ha una de veritat: els correus de reset sortien de debò. Ara cal un EMAIL_TRANSPORT=sendgrid explícit.
- findByID llança una excepció, no retorna null: cinc trucades el tractaven com a null i un id inexistent donava 500. Ara, 404.
- Hi havia onze PGPASSWORD en clar dins scripts de test; fora del HEAD. La rotació de la contrasenya és el que queda, i és meva: la cadena viu a l'historial de GitHub.
- Les API keys dels proveïdors d'IA es guardaven en clar; ara aes-256-gcm amb la clau derivada de l'APP_SECRET, amb migració suau per a les existents.
Com queda
El TASKS.md té 40 feines; el bloc P0, tancat. Toca el P1.