Files
clienthub/.planning/security/SECURITY-REMEDIATION-PLAN.md
simone f7eb7eec23 docs(planning): archivia v2.1/v2.2/v2.3 e documenta v2.4
.planning/ documentava in dettaglio cio che era vecchio e per niente cio
che e in produzione: le fasi 11-22 (v2.1 e v2.2, chiuse a giugno) erano
ancora in phases/ mentre v1.0 e v2.0 stavano gia in milestones/, e il
lavoro degli ultimi due mesi - gate OTP e ciclo di vita dei retainer, cioe
quello che gira su hub.iamcavalli.net - non aveva nessuna cartella.

- phases/{11,12,14} -> milestones/v2.1-phases/, phases/{18..22} ->
  milestones/v2.2-phases/. Ora phases/ contiene solo la milestone in
  corso, che e quello che state.cjs conta per il progresso
- v2.1-ROADMAP.md ricostruito: era l'unica milestone senza archivio,
  interrotta dal reset del 19/06 e mai chiusa formalmente
- v2.3-ROADMAP.md + v2.3-REQUIREMENTS.md: v2.3 e stata eseguita fuori dal
  ciclo GSD, non esistono PLAN/SUMMARY per fase. L'archivio E la doc
- REQUIREMENTS.md riscritto per v2.4 con il backlog reale
- phases/13 e phases/26: SUMMARY ricostruiti da commit, migration e
  STATUS.md. 26 e il primo numero libero
- research/: cancellate 4 varianti dello stesso PITFALLS e FEATURES/
  SUMMARY, superati da PROJECT.md. Diverse anti-feature erano ormai
  contraddette dai fatti (il Kanban e stato costruito in Phase 19,
  l'email in v2.3, il time tracking esiste)
- cancellati UI-RULES.md e DESIGN-SYSTEM.md (CLAUDE.md li dichiara
  superseded: impongono l'inverso della regola attuale) e HANDOFF.md,
  fermo al 13/06
- SECURITY-*.md -> security/: audit chiuso, ma i report restano la doc di
  cosa e stato ruotato e perche
- PROJECT.md/MILESTONES.md/ROADMAP.md allineati: milestone corrente v2.4,
  sessione OTP 90gg non 30, migrazioni fino alla 0016

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 22:38:36 +02:00

159 lines
7.8 KiB
Markdown
Raw Permalink 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 | ❎ **non serve** — la password leakata risulta già morta (vedi correzione in INFRA §1) |
| 0.1b | Espurgare la password dai due `07-01-SUMMARY.md` | ✅ fatto |
| 0.2 | `INTERNAL_SECRET` in Coolify | ✅ fatto 2026-07-28 — le route interne ora danno 403 |
| 0.3 | Allungare `ADMIN_PASSWORD` | ✅ fatto 2026-07-28 — 15 → 32 caratteri |
| 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_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.