# 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:@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**: ```ts 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=` 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`, `/quote` — **non** `/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.0–4.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.