I 4 slug esistenti avevano suffissi da 4 caratteri generati con Math.random(); ora 12 caratteri CSPRNG. Lo slug risolve prima del token, quindi era il vero anello debole dell'accesso alla dashboard cliente (C-2). Lo script SQL NON e' committato di proposito: contiene gli slug in chiaro, che sono credenziali bearer verso /client/<slug> — committarlo ripeterebbe la fuga di credenziali trovata al punto 1 dell'audit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.7 KiB
Piano di remediation sicurezza — ClientHub
Data: 2026-07-27 · Base: .planning/SECURITY-SCAN.md + .planning/SECURITY-AUDIT-INFRA.md
Stato: APPROVATO ed ESEGUITO per la parte codice (2026-07-27).
Restano aperti solo i punti che richiedono accesso a Coolify o una tua decisione — vedi in fondo.
11 findings totali: 2 critici, 4 alti, 3 medi, 2 bassi.
Stato di esecuzione
| # | Intervento | Stato |
|---|---|---|
| 0.1 | Ruotare password Postgres prod | ⛔ richiede te (no accesso Coolify) |
| 0.1b | Espurgare la password dai due 07-01-SUMMARY.md |
✅ fatto |
| 0.2 | INTERNAL_SECRET in Coolify |
⛔ richiede te |
| 0.3 | Allungare ADMIN_PASSWORD |
⛔ richiede te |
| 1.1 | Next → 16.2.12, next-auth → 4.24.15 | ✅ fatto |
| 1.2 | Secondo gate di autenticazione admin | ✅ fatto |
| 1.3 | Eliminare src/lib/quote-actions.ts |
✅ fatto |
| 1.4 | Rate limit su /client/ |
✅ fatto |
| 2.1 | Slug a 12 caratteri + CSPRNG | ✅ fatto (solo nuovi clienti) |
| 2.2 | Rigenerare gli slug esistenti | ✅ fatto — 4 clienti ruotati in prod il 2026-07-28 |
| 3.1 | HSTS | ✅ fatto |
| 3.2 | CSP | ✅ fatto (enforcing; script-src con unsafe-inline, motivato nel file) |
| 3.3 | Rimuovere i 4 sink dangerouslySetInnerHTML |
✅ fatto |
| 3.4 | Delimitare i transcript nel prompt | ✅ fatto |
| 3.5 | Potatura della Map di rate-limit.ts |
✅ fatto |
Verifiche eseguite: tsc --noEmit pulito · npm run build OK (32 route) · eslint pulito sui file
toccati · smoke test su /admin/login (rende), /admin (307 → login), header forgiati (307 → login,
non servono la pagina) · CSP e HSTS presenti nella risposta.
npm audit: da 12 vulnerabilità (1 critica, 5 alte) a 10 (0 critiche, 5 alte). Le 9 CVE dirette di
Next.js — incluso il proxy bypass — sono chiuse; il flag next residuo è solo transitivo via
postcss/sharp vendorizzati dentro Next, e l'unico "fix" che npm propone è il downgrade a Next 9.
js-yaml e brace-expansion restano ma sono solo toolchain di sviluppo.
Ordine di esecuzione
Ordinato per rischio ora, non per difficoltà. I lotti 0 e 1 chiudono tutto ciò che è attivamente sfruttabile oggi.
LOTTO 0 — Rotazione segreti (nessun codice, solo credenziali) · ~30 min
Va per primo perché è l'unico finding dove il segreto è già uscito dal perimetro.
0.1 — Ruotare la password Postgres di produzione (INFRA §1)
- Nuova password su Postgres prod via
ssh+docker exec ... psql(ALTER USER clienthub WITH PASSWORD ...) - Aggiornare
DATABASE_URLnelle env var di Coolify → redeploy - Aggiornare
.env.locallocale - Espurgare la stringa dai due file
.planning/**/07-01-SUMMARY.mde committare - ⚠️ La credenziale resta nella storia di git: la rotazione è ciò che la neutralizza, la cancellazione del file no. Non serve riscrivere la storia se la password è cambiata.
0.2 — Impostare INTERNAL_SECRET in Coolify produzione (INFRA §2)
- Aggiungere la env var (valore già presente in
.env.local, 44 char) → redeploy - Verifica di accettazione:
curl -sI 'https://hub.iamcavalli.net/api/internal/validate-slug?slug=x'deve rispondere403, non404. Oggi risponde404.
0.3 — Allungare ADMIN_PASSWORD (INFRA §7) — oggi 14 char, il .env.example prescrive ≥20.
È l'unico fattore di autenticazione admin.
Rischio dati: nullo. Nessuna migrazione, nessuno schema toccato.
LOTTO 1 — Chiudere lo sfruttabile · ~2 ore
1.1 — Aggiornare Next.js a ≥ 16.2.11 e next-auth a ≥ 4.24.15 (INFRA §3, §4)
- Chiude il proxy-bypass GHSA-6gpp-xcg3-4w24 (che è il moltiplicatore di C-1),
la disclosure degli endpoint delle Server Function, 2 SSRF, 2 cache-confusion, 3 DoS,
e il crash di
getToken()su headerAuthorizationmalformato. - Update mirati, non
npm audit fix --force: quel comando tenta di degradaredrizzle-kita 0.18.1 (downgrade major) e romperebbe le migrazioni. - Trascina anche i fix di
postcssesharp. - Serve un
npm run build+ smoke test su login admin e una dashboard cliente.
1.2 — Rendere admin/layout.tsx un guard vero (C-1) — una riga:
if (!session) redirect("/admin/login");
Da solo trasforma il singolo punto di rottura in due livelli indipendenti. Anche con il proxy aggiornato, questo è ciò che rende il sistema robusto al prossimo bug del proxy.
1.3 — Eliminare src/lib/quote-actions.ts (C-3) — codice morto, zero chiamanti,
due endpoint pubblici non autenticati di cui uno scrive su DB. Cancellazione secca.
In alternativa conservativa: requireAdmin() in testa a entrambe le funzioni.
1.4 — Rate limit sul ramo /client/ in src/proxy.ts (C-2) — oggi il rate limiter è
applicato solo a /quote/. Toglie la possibilità di brute-forzare gli slug a velocità utile.
Rischio dati: nullo. Nessuna migrazione.
LOTTO 2 — Rinforzare gli slug · ~2 ore · ⚠️ tocca dati esistenti
2.1 — Portare il suffisso di toSlug() da 4 a 12 caratteri casuali (C-2)
src/app/admin/clients/new/actions.ts:22-30- Da 36⁴ ≈ 1,7 milioni a 36¹² ≈ 4,7 × 10¹⁸.
2.2 — Rigenerare gli slug dei clienti esistenti
- ⚠️ Rompe i link già inviati ai clienti. Da decidere insieme:
- (a) rigenerare tutto e reinviare i link, oppure
- (b) lasciare gli slug esistenti e affidarsi al rate limit di 1.4 come mitigazione.
- Migrazione puramente additiva su una colonna esistente (
UPDATE clients SET slug=...): nessunDROP, nessunTRUNCATE, conforme al vincolo Data Safety di CLAUDE.md. - Questa è una decisione tua, non mia. Il lotto 2 non parte senza una risposta su (a) vs (b).
LOTTO 3 — Difesa in profondità · ~2 ore
3.1 — Strict-Transport-Security in next.config.ts (INFRA §6)
max-age=63072000; includeSubDomains; preload. Conta più del normale qui: i token cliente
viaggiano nell'URL, quindi un downgrade attivo li espone in chiaro.
3.2 — Content-Security-Policy (INFRA §6) — richiede un nonce per lo script inline di tema
in src/app/layout.tsx:38. Da introdurre prima in Report-Only per una settimana, poi in enforcing.
3.3 — Sanificare i quattro sink dangerouslySetInnerHTML (C-4) in
src/components/public/proposal/sections/. Dato che serve solo grassetto/corsivo, la strada
più pulita non è aggiungere una libreria di sanitizzazione ma togliere il rendering HTML
e sostituirlo con un formatter a whitelist di tag.
3.4 — Delimitare i transcript nel prompt di src/lib/proposal/agent.ts:37-40, così che
il contenuto fornito da terzi non possa essere confuso con istruzioni.
3.5 — Potare la Map di src/lib/rate-limit.ts (INFRA §7) — oggi cresce di una entry per
IP distinto e non viene mai svuotata: crescita di memoria non limitata nel container.
Cosa NON propongo di fare
- Non riscrivere la storia di git per il leak §1: la rotazione neutralizza la credenziale,
e un
filter-branchsu un repo con storia condivisa costa più di quanto renda. - Non toccare
drizzle-kit/esbuild: le uniche vulnerabilità restanti sono di sola toolchain di sviluppo, non raggiungibili in produzione, e il "fix" è un downgrade major. - Non introdurre 2FA sul login admin in questo giro: è un cambio di prodotto, non una correzione. Da valutare insieme alla feature Email OTP già a design.
Riepilogo
| Lotto | Contenuto | Tempo | Migrazioni | Rischio dati |
|---|---|---|---|---|
| 0 | Rotazione segreti | ~30 min | no | nullo |
| 1 | Update + guard + dead code + rate limit | ~2 h | no | nullo |
| 2 | Rinforzo slug | ~2 h | UPDATE additivo | richiede tua decisione |
| 3 | HSTS, CSP, XSS, prompt, memoria | ~2 h | no | nullo |
I lotti 0 e 1 chiudono tutto ciò che è sfruttabile oggi e non toccano un solo dato.