fb6ab92fd0
CORREZIONE: la password Postgres trovata in git NON era attiva. La verifica iniziale si limitava a constatare che la stringa comparisse in .env.local e ne deduceva che fosse quella viva. Il confronto del verifier SCRAM-SHA-256 di pg_authid contro i due candidati mostra che quella committata non combacia: era gia stata ruotata. La voce DATABASE_URL porta 5432 di .env.local e' stale. Severita' reale: BASSA, non CRITICA. Nessuna rotazione necessaria. Chiusi via API Coolify: INTERNAL_SECRET creata (le route /api/internal/* rispondono ora 403 invece di 404, oracolo di enumerazione token chiuso) e ADMIN_PASSWORD portata da 15 a 32 caratteri. Redeploy verificato. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
7.8 KiB
Markdown
159 lines
7.8 KiB
Markdown
# 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.
|