Files
clienthub/.planning/SECURITY-REMEDIATION-PLAN.md
T
simone e2bd1d95ed fix(security): audit completo — secondo gate admin, hardening slug, XSS, CSP/HSTS, update CVE
Audit di sicurezza su tutta l'app. Report in .planning/SECURITY-SCAN.md (codice),
.planning/SECURITY-AUDIT-INFRA.md (dipendenze/segreti/deploy) e piano in
.planning/SECURITY-REMEDIATION-PLAN.md.

CRITICO — l'autorizzazione admin era un unico punto di rottura: nessuna delle 21
pagine /admin controllava la sessione e admin/layout.tsx renderizzava comunque i
figli quando mancava. L'unico guard era proxy.ts, su un Next.js affetto da
GHSA-6gpp-xcg3-4w24 (proxy bypass). Ora il layout è un secondo gate indipendente;
proxy.ts marca il path con un token derivato da NEXTAUTH_SECRET, così il gate non
è aggirabile forgiando header e fallisce chiuso se il proxy non gira.

ALTO — gli slug cliente avevano 4 caratteri casuali da Math.random() (~20 bit,
1.7M tentativi) e risolvono prima del token: ora 12 caratteri via nanoid
(CSPRNG, ~62 bit). Aggiunto rate limit al ramo /client/, che ne era privo.

ALTO — src/lib/quote-actions.ts esponeva due server action pubbliche senza
autenticazione, una delle quali scriveva su DB. Codice morto, zero chiamanti:
rimosso.

MEDIO — i quattro dangerouslySetInnerHTML nelle sezioni proposta rendevano output
AI come HTML grezzo su pagina pubblica, alimentato da transcript di terzi. Sostituiti
con RichText (whitelist di emphasis, nessun HTML al DOM). I transcript ora sono
recintati in tag che il system prompt dichiara essere dati, non istruzioni.

Inoltre: next 16.2.6 -> 16.2.12 e next-auth 4.24.14 -> 4.24.15 (chiude 9 CVE Next
piu GHSA-xmf8-cvqr-rfgj su getToken, raggiungibile dal proxy); HSTS e CSP;
potatura della Map di rate-limit.ts, che cresceva senza limite; espunta la password
Postgres di produzione dai due 07-01-SUMMARY.md.

Verificato: tsc pulito, build OK, smoke test su login/redirect/header forgiati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:35:46 +02:00

159 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | ⛔ **richiede tua decisione (a) o (b)** |
| 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_URL` nelle env var di Coolify → redeploy
- Aggiornare `.env.local` locale
- Espurgare la stringa dai due file `.planning/**/07-01-SUMMARY.md` e 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 rispondere `403`, non `404`. Oggi risponde `404`.
**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 header `Authorization` malformato.
- Update mirati, **non** `npm audit fix --force`: quel comando tenta di degradare
`drizzle-kit` a 0.18.1 (downgrade major) e romperebbe le migrazioni.
- Trascina anche i fix di `postcss` e `sharp`.
- 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:
```ts
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=...`):
nessun `DROP`, nessun `TRUNCATE`, 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-branch` su 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.