Rimossi i doppioni e gli artefatti accumulati, senza cancellare nulla di definitivo: tutto cio' che serviva una revisione e' parcheggiato in cestino/ (gitignored), documentato in cestino/LEGGIMI.md. - .planning/phases/01-10: 10 cartelle identiche byte-per-byte alle copie in .planning/milestones/v1.0-phases e v2.0-phases. HANDOFF.md:33 documentava che furono copiate e non spostate, lasciando la pulizia 'facoltativa in futuro'. Verificata l'identita' con diff -rq prima di spostare ciascuna. - scripts/: 13 script one-off gia' eseguiti (push-*, migrate-*, validate-*, verify-12-03-*) piu' reset-and-import-services.ts, che cancella dati. Restano i 3 riutilizzabili: seed, import-services-notion, import-service-offer-tags. - CLAUDE-SECURITY-20260727-210226/: cartella di lavoro della run interrotta. Cancellati subito, senza revisione: 6 .DS_Store, le due cache .impeccable/ (una era dentro src/) e tsconfig.tsbuildinfo. .gitignore: aggiunti cestino/, .impeccable/ e CLAUDE-SECURITY-*/ per evitare che si riformino. src/ non e' stato toccato: la struttura e' dettata dall'App Router di Next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.0 KiB
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 → BASSO — Password Postgres storica committata in chiaro
CORREZIONE (2026-07-28). La prima stesura di questa sezione dichiarava la credenziale "ancora attiva". Era sbagliato. La verifica iniziale si limitava a constatare che la stringa comparisse anche in
.env.local, e ne deduceva che fosse quella viva.Verifica corretta, fatta poi: confronto del verifier SCRAM-SHA-256 di
pg_authidcontro i due candidati. La password committata non combacia; quella in uso è la password da 48 caratteri configurata su Coolify. Era già stata ruotata in passato.La voce
DATABASE_URLporta 5432 di.env.localè stale — residuo storico, non la connessione di produzione (che passa dall'hostname interno del container).Severità reale: BASSA. Credenziale storica in un repo privato, già morta. Igiene, non incidente. Nessuna rotazione urgente. Resta utile ripulire
.env.localdalla voce stale.Il resto della sezione è conservato come traccia di ciò che è stato trovato.
Dove:
.planning/milestones/v2.0-phases/07-unified-service-catalog/07-01-SUMMARY.md:188,195,202- (esisteva anche in
.planning/phases/07-unified-service-catalog/, cartella duplicata spostata incestino/il 2026-07-28 durante la pulizia — la stringa era già stata espunta da entrambe le copie)
Cosa: stringa completa in plaintext, dentro git, con credenziale reale:
postgresql://clienthub:<PASSWORD-IN-CHIARO>@178.104.27.55:5432/clienthub?sslmode=disable
Verificato (versione corretta): la password presente in quei file non è più attiva —
il verifier SCRAM del DB non combacia. Coincideva solo con la voce DATABASE_URL porta 5432
di .env.local, che è a sua volta stale. Era già stata ruotata prima di questo audit.
Aggravanti — valgono solo per il periodo in cui la credenziale era viva:
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, /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_PASSWORDin.env.localè di 14 caratteri;.env.exampleprescrive "min 20 chars". È l'unico fattore di autenticazione admin (nessun 2FA, nessun lockout — vedi §8).rateLimit()insrc/lib/rate-limit.tsè in-memory e laMapnon viene mai potata: cresce di una entry per IP distinto, senza limite → crescita di memoria non limitata..dockerignoreesclude correttamente.enve.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.