Commits
Auditing our own platform: the security block, closed
Full review of the project: 40 tasks in TASKS.md, and the whole P0 security block closed in one day. Including a JWT nobody was verifying and a broken restore path.
Where it started
In the morning I closed a long-overdue job: onboarding for external collaborators. The specs (46 files) were the phase reference and lived outside the repo; they are now in specs/, with a docs/ONBOARDING.md covering everything from cloning the repo to having admin access, and a .agents/ folder with commands and pitfalls for agents from other machines.
Then I did the full review I had been putting off for weeks. It produced a docs/TASKS.md with 40 tasks ordered into blocks: P0 security, P1 data integrity, P2 accessibility, P3 performance. Whatever was checked live against the dev server got a ⚠; whatever was only suspected, a ?.
The biggest hole: a JWT nobody verified
Four endpoints resolved the user by hand-decoding the second segment of the token, without checking the signature or the expiry. In practice, this:
GET /api/publicitat/estadistiques without cookie -> 401
GET /api/publicitat/estadistiques fake cookie -> 200
The fake cookie was literally payload-token=<base64 of {"id":2}>.madeup. Now all user resolution goes through payload.auth() from src/lib/session.ts, a single point. Two more holes fell along the way: any user could promote themselves to admin with a PATCH on their own row (the guard only covered create), and Media was the only collection with no declared write access.
The drill that exposed a broken restore
The backup is complete; the restore path is not. I restored the day's dump into a test database and the tables matched production, but restore.sh had three chained problems:
- It looked for the backup in the wrong place: it read the old system from ~/backups/, stopped since September 9th, and not the container's daily one.
- The dump is generated by pg_dump 18 against a Postgres 16.13 and carries SET transaction_timeout, which doesn't exist in 16. With ON_ERROR_STOP=1 it aborted on the first line and left 0 tables; without it it "worked" by accident.
- Output went to /dev/null and counted nothing: an empty restore ended up saying OK.
It now filters the incompatible SETs, turns on ON_ERROR_STOP and counts tables at the end: fewer than 10, exit 1. Verified: with the filter, 57 tables and agenda 1450; without, zero.
The rest of the list
- The rate limit for LaIA and subscriptions was dodgeable: four routes read the FIRST value of X-Forwarded-For, the one the visitor controls. Now the last one is taken, the one added by the proxy, and the limiter lives entirely in src/lib/ip.ts.
- Ad beacons went unsigned: hammering /ads/imp/ inflated statistics and could exhaust a paid campaign from outside. The banner is now issued with nonce + HMAC.
- SendGrid activated on the mere presence of the key, and dev has a real one: reset emails actually went out. An explicit EMAIL_TRANSPORT=sendgrid is now required.
- findByID throws an exception, it doesn't return null: five calls treated it as null and a missing id gave a 500. Now, a 404.
- There were eleven PGPASSWORDs in clear text inside test scripts; out of HEAD. Rotating the password is what's left, and it's mine: the string lives in GitHub history.
- The AI providers' API keys were stored in clear; now aes-256-gcm with the key derived from APP_SECRET, with a soft migration for existing ones.
Where it stands
TASKS.md has 40 tasks; the P0 block, closed. On to P1.