Files
clienthub/.planning/SECURITY-AUDIT-INFRA.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

6.7 KiB
Raw Blame History

Security Audit — Infra / Dependencies / Secrets lane

Data: 2026-07-27 · Branch: main · Commit: dd2d148 Autore: audit manuale (lane complementare allo scan multi-agente del plugin claude-security, il cui report vive in .planning/SECURITY-SCAN.md).

Questo file copre ciò che lo scan del codice non guarda: dipendenze, segreti in git, configurazione di deploy e superficie di rete in produzione.


1. CRITICO — Password Postgres di produzione committata in chiaro

Dove:

  • .planning/phases/07-unified-service-catalog/07-01-SUMMARY.md:188,195,202
  • .planning/milestones/v2.0-phases/07-unified-service-catalog/07-01-SUMMARY.md:188,195,202

Cosa: stringa completa in plaintext, dentro git, con credenziale reale:

postgresql://clienthub:<PASSWORD-IN-CHIARO>@178.104.27.55:5432/clienthub?sslmode=disable

Verificato: la password presente in quei file è ancora attiva — coincide con la voce DATABASE_URL (porta 5432) di .env.local. Non è stata ruotata.

Aggravanti:

  • sslmode=disable → traffico Postgres in chiaro sulla rete.
  • Il commit è nella storia di git: cancellare il file non basta, la credenziale resta recuperabile da qualunque clone o dal remote Gitea.

Impatto: chiunque abbia (o abbia avuto) accesso in lettura al repo Gitea, o a un clone locale, possiede la credenziale del DB di produzione — che contiene clients, payments, projects.

Nota: la seconda DATABASE_URL di .env.local (porta 54321, quella effettivamente usata in runtime secondo project_phase11_pending_migration) ha una password diversa e non risulta leakata. Il leak riguarda la 5432.


2. ALTO — INTERNAL_SECRET non configurato in produzione → oracolo di enumerazione token

Dove: src/app/api/internal/validate-token/route.ts, src/app/api/internal/validate-slug/route.ts

Le due route applicano il segreto solo se la env var è presente:

const secret = process.env.INTERNAL_SECRET;
if (secret && request.headers.get("x-internal-secret") !== secret) { ...403 }

Verificato in produzione (https://hub.iamcavalli.net), 27/07/2026:

Richiesta Atteso se il segreto fosse attivo Osservato
GET /api/internal/validate-slug?slug=<inesistente> senza header 403 404
stessa richiesta con x-internal-secret: wrong 403 404
GET /api/internal/validate-slug senza parametro 400 {"valid":false} (route viva)

Il fallback ?? "" in src/proxy.ts maschera il problema: il proxy funziona lo stesso, quindi il difetto è invisibile in esercizio.

Impatto: le route sono pubblicamente raggiungibili da Internet (il matcher del proxy copre solo /admin, /client, /quotenon /api/internal), senza autenticazione e senza rate limit. Sono un oracolo binario valido/non-valido per token e slug dei clienti: distinguono 404 (non esiste) da 200 (esiste). Un attaccante può forzare slug brevi e prevedibili e ottenere l'accesso completo alla dashboard di un cliente, che è l'unico controllo d'accesso del portale.

.env.local ha INTERNAL_SECRET valorizzato (44 char) → è una lacuna della config Coolify, non del codice.


3. ALTO — Next.js 16.2.6 vulnerabile a bypass del Proxy/Middleware

next@16.2.6 installato. Advisory rilevanti (tutte fixate in 16.2.11):

Advisory Titolo
GHSA-6gpp-xcg3-4w24 Middleware / Proxy bypass in App Router
GHSA-955p-x3mx-jcvp Unauthenticated disclosure of internal Server Function endpoints
GHSA-89xv-2m56-2m9x SSRF in Server Actions on custom servers
GHSA-p9j2-gv94-2wf4 SSRF in rewrites via attacker-controlled destination hostname
GHSA-68g3-v927-f742 / GHSA-4633-3j49-mh5q Cache confusion of response bodies
GHSA-m99w-x7hq-7vfj / GHSA-4c39-4ccg-62r3 DoS via Server Actions
GHSA-q8wf-6r8g-63ch DoS in Image Optimization API (SVG)

Perché è grave qui in particolare: l'intera autorizzazione admin di questa app poggia su src/proxy.ts. Un bypass del proxy = accesso non autenticato a /admin/*. Non c'è un secondo livello di difesa a livello di pagina.


4. CRITICO (npm) — next-auth 4.24.14

Advisory CVSS Titolo
GHSA-xmf8-cvqr-rfgj 7.5 getToken() solleva un'eccezione non gestita su header Authorization: Bearer malformato
GHSA-x445-f3h2-j279 6.8 cookie di state/nonce/PKCE non legati al provider
GHSA-7rqj-j65f-68wh bypass omoglifo @ nel normalizzatore email

src/proxy.ts chiama getToken() su ogni richiesta /admin/*. GHSA-xmf8-cvqr-rfgj è quindi direttamente raggiungibile: un header Authorization malformato fa esplodere il guard. Fix: next-auth ≥ 4.24.15. Gli altri due non si applicano (nessun provider OAuth, nessun login via email).


5. MEDIO — Altre dipendenze

Pacchetto Sev Note
postcss ≤8.5.17 high transitiva via next; si risolve aggiornando next
sharp <0.35.0 high CVE libvips; transitiva via next
brace-expansion ≤5.0.7 high DoS, solo toolchain di sviluppo
js-yaml 4.0.04.2.0 high DoS, solo dev
uuid <11.1.1 moderate transitiva via next-auth
drizzle-kit / esbuild moderate solo dev; il fix è un downgrade major → non applicare

Totale npm audit: 12 vulnerabilità (1 critica, 5 alte, 5 moderate, 1 bassa).


6. MEDIO — Header di sicurezza incompleti

next.config.ts imposta X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy. Mancano (verificato su risposta live):

  • Strict-Transport-Security — assente. Il portale è interamente HTTPS; senza HSTS un downgrade attivo espone i token cliente, che viaggiano nell'URL.
  • Content-Security-Policy — assente. Nessuna mitigazione di secondo livello contro XSS.

7. BASSO — Ipotesi da confermare

  • ADMIN_PASSWORD in .env.local è di 14 caratteri; .env.example prescrive "min 20 chars". È l'unico fattore di autenticazione admin (nessun 2FA, nessun lockout — vedi §8).
  • rateLimit() in src/lib/rate-limit.ts è in-memory e la Map non viene mai potata: cresce di una entry per IP distinto, senza limite → crescita di memoria non limitata.
  • .dockerignore esclude correttamente .env e .env.local: nessun segreto nell'immagine.
  • Nessuna credenziale trovata in file sorgente tracciati né nel resto della storia di git, oltre al caso §1.

8. Nota di contesto — nessun lockout sul login admin

src/lib/auth.ts confronta email/password con le env var e non ha né rate limit né lockout (il matcher del proxy include /admin/* ma /admin/login è esplicitamente escluso dal guard, e /api/auth/* non è coperto dal rate limiter). Da correlare con l'esito dello scan del codice.