# Audit di sicurezza — EvoAz & Supernova

> **Data:** 2026-07-10
> **Scope:** backend Node.js/Express dei progetti `gix-evoaz` e `gix-supernova` (codebase gemelle), config Apache, gestione segreti, dipendenze.
> **Metodologia:** revisione manuale del codice (auth, routing, controller pubblici e admin, upload, webhook, servizi MinIO/DocuSeal/email), diff tra i due progetti, `npm audit`, ispezione `.env` e vhost.

I due progetti condividono la stessa architettura e quasi tutto il codice. **Le vulnerabilità qui elencate si applicano a entrambi**, salvo dove indicato. Differenze rilevanti:

| Aspetto | EvoAz | Supernova |
|---|---|---|
| Modulo Contratti/DocuSeal | ✅ presente | ❌ non ancora migrato |
| Rate-limit sui form pubblici (`/apply`, `/contact`) | ❌ **assente** | ✅ presente (`formLimiter`) |
| Rate-limit login | ✅ | ✅ |
| Backend infra | stesso DB host, **stesso account MinIO**, bucket diversi | idem |

---

## Riepilogo findings

| # | Severità | Titolo | Progetto |
|---|---|---|---|
| 0 | 🔴 **CRITICO** | GitHub PAT (`ghp_…`) in chiaro nel remote git (`.git/config`) | **Supernova** (verificare EvoAz) |
| 1 | 🔴 **CRITICO** | Credenziali MinIO reali in chiaro, versionate su Git (`appunti_minio.txt`) | EvoAz (non presente in Supernova) |
| 2 | 🟠 **ALTO** | Link onboarding pubblico riutilizzabile: `used_at` mai impostato/controllato | entrambi |
| 3 | 🟠 **ALTO** | Password temporanee admin generate con `Math.random()` (non CSPRNG) | entrambi |
| 4 | 🟡 **MEDIO** | Nessun rate-limit su `/apply` e `/contact` | **solo EvoAz** |
| 5 | 🟡 **MEDIO** | Dipendenze npm vulnerabili (nodemailer 3 CVE, qs DoS) | entrambi |
| 6 | 🟡 **MEDIO** | Fallback JWT_SECRET hardcoded attivo fuori da `NODE_ENV=production` | entrambi |
| 7 | 🟡 **MEDIO** | Nessun controllo di autorizzazione per ruolo (ogni utente = super-admin) | entrambi |
| 8 | 🔵 **BASSO** | `used_at` presente nell'onboarding ma il flag di debug HMAC può loggare PII in chiaro | entrambi |
| 9 | 🔵 **BASSO** | Endpoint onboarding pubblico espone tutta l'anagrafica (CF, IBAN, doc) via solo token | entrambi |
| 10 | 🔵 **BASSO** | File sensibili `.env` / `google-oauth2-credentials.json` con permessi `644` | entrambi |

---

## Dettaglio

### 🔴 0 — GitHub Personal Access Token in chiaro nel remote git

**File:** `gix-supernova/.git/config` (rilevato via `git remote -v`).

Il remote `origin` di Supernova incorpora un **PAT GitHub in chiaro** nell'URL:

```
https://destefanix:ghp_UQSk…2pPk@github.com/destefanix/gixsupernova-site.git
```

Chiunque legga `.git/config`, esegua `git remote -v`, o abbia accesso a un backup/clone del filesystem ottiene un token che dà accesso in scrittura ai repository GitHub dell'utente (l'ampiezza dipende dagli scope del token — un `repo` classico dà accesso a **tutti** i repo privati). Verificare anche il remote di EvoAz (`git remote -v` in `gix-evoaz`): al momento dell'audit mostrava un URL HTTPS senza token, ma va riconfermato.

**Rimedio:**
1. **Revocare subito** il token su GitHub → Settings → Developer settings → Personal access tokens.
2. Riconfigurare il remote senza credenziali inline:
   ```bash
   git remote set-url origin https://github.com/destefanix/gixsupernova-site.git
   ```
   e usare un **credential helper** (`git config --global credential.helper store` con un token a scope minimo, o SSH keys, o `gh auth login`).
3. Preferire una **deploy key SSH per-repo** (accesso limitato al singolo repository) invece di un PAT personale onnicomprensivo sui server di produzione.

---

### 🔴 1 — Credenziali MinIO reali in chiaro e versionate

**File:** [appunti_minio.txt](appunti_minio.txt) — **tracciato in git** (`git ls-files` lo conferma) e pushato su `github.com/destefanix/gixevoaz-site`.

Il file contiene le credenziali di produzione **non mascherate**:

```
MINIO_ACCESS_KEY=gixflowadmin
MINIO_SECRET_KEY=HAujhuhgds…7ghu   ← secret reale in chiaro
```

Questo account (`gixflowadmin` su `storage.gixflow.cloud`) serve **tutti** i progetti dell'infrastruttura (evoaz, supernova e presumibilmente gli altri `gix-*`), non solo il bucket di questo progetto. Chi ottiene questo secret ha accesso a tutti i bucket (CV, documenti d'identità, IBAN, contratti firmati).

Il repo risulta **privato** — questo attenua ma non elimina il rischio: le credenziali restano nella history git di ogni clone locale, di ogni fork, e sono esposte a chiunque abbia (o ottenga) accesso al repository o a un backup.

**Rimedio:**
1. **Ruota subito** access/secret key MinIO (rigenerare le chiavi su MinIO).
2. Rimuovi il file dalla history git, non solo dal working tree:
   ```bash
   git rm appunti_minio.txt
   # e riscrivi la history: git filter-repo --path appunti_minio.txt --invert-paths
   ```
   Aggiungi `appunti_minio.txt` (e ogni file "appunti") a `.gitignore`.
3. Valuta credenziali MinIO **dedicate per progetto** con policy limitate al singolo bucket, invece dell'admin condiviso.
4. Anche `backend/src/schema.sql` è versionato: verifica che non contenga dati/segreti (lo schema puro è ok, ma controlla eventuali INSERT).

---

### 🟠 2 — Link di onboarding pubblico riutilizzabile all'infinito

**File:** [backend/src/controllers/onboardingController.js:120](backend/src/controllers/onboardingController.js#L120) (`submitOnboardingForm`) e `getOnboardingForm`.

Il token onboarding (valido 7 giorni) permette a un utente non autenticato di:
- **leggere** tutta l'anagrafica del candidato (codice fiscale, IBAN, dati documento) — `getOnboardingForm`;
- **sovrascrivere** quei dati e caricare documenti — `submitOnboardingForm`.

La colonna `used_at` **esiste** nello schema ma non viene **mai impostata a `NOW()` dopo il submit, né controllata**. Il link quindi:
- resta valido e **multi-uso** per 7 giorni interi;
- consente reinvii/sovrascritture ripetute anche dopo il completamento;
- se il link finisce nella cronologia browser, in un proxy o in un log email, chiunque lo apra vede CF/IBAN.

Confronto: l'endpoint pubblico dei test formazione ([trainingController.js:549](backend/src/controllers/trainingController.js#L549)) gestisce **correttamente** `used_at` — l'onboarding no.

**Rimedio:**
- In `submitOnboardingForm`, dopo l'update, marca il token: `UPDATE onboarding_tokens SET used_at = NOW() WHERE id = ?`.
- In `getOnboardingForm` e `submitOnboardingForm`, rifiuta i token con `used_at IS NOT NULL` (o consenti un numero limitato di modifiche entro un tempo breve dal primo accesso).
- Aggiungi rate-limit sull'endpoint `/onboarding/:token/*`.

---

### 🟠 3 — Password temporanee admin con generatore non crittografico

**File:** [backend/src/controllers/usersController.js:7](backend/src/controllers/usersController.js#L7) (`generateTempPassword`).

```js
for (let i = 0; i < 12; i++) pwd += chars[Math.floor(Math.random() * chars.length)]
```

`Math.random()` **non è un CSPRNG**: il suo output è predicibile. Queste password proteggono la **creazione di nuovi account admin** e i **reset via "reinvia benvenuto"**. Un attaccante che osservi/deduca lo stato del PRNG può prevedere la password temporanea inviata via email a un nuovo admin.

**Rimedio:** usare `crypto.randomBytes` / `crypto.randomInt`, già importato altrove nel progetto:
```js
import crypto from 'crypto'
function generateTempPassword() {
  const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$'
  let pwd = ''
  for (let i = 0; i < 16; i++) pwd += chars[crypto.randomInt(chars.length)]
  return pwd
}
```

---

### 🟡 4 — Nessun rate-limit sui form pubblici (solo EvoAz)

**File:** [backend/src/index.js](backend/src/index.js) — EvoAz applica `loginLimiter` solo a login/2FA. Gli endpoint pubblici `POST /api/apply` e `POST /api/contact` **non hanno rate-limit**, esponendo a spam di massa, abuso di invio email e riempimento del DB/storage con CV.

Supernova ha già la fix (`formLimiter`, max 20/15min) — **va portata su EvoAz**. Ecco un caso in cui i due progetti sono già divergenti: allineare EvoAz a Supernova.

**Rimedio (EvoAz):** copiare il blocco `formLimiter` da `gix-supernova/backend/src/index.js` e applicarlo a `/api/apply` e `/api/contact`.

---

### 🟡 5 — Dipendenze npm vulnerabili

`npm audit` (solo prod) segnala **5 vulnerabilità (2 high, 3 moderate)**:

- **nodemailer** (attuale `^7.0.5`) — 3 advisory, tra cui *Improper TLS Certificate Validation in OAuth2 Token Fetch* (intercettazione credenziali) e file-read/SSRF via opzioni `raw`/`jsonTransport`. Fix in `nodemailer@9` (breaking).
- **qs** `6.11.x` — DoS remoto in `qs.stringify`.

**Rimedio:** `npm audit fix` per le non-breaking; pianificare l'upgrade a `nodemailer@9` testando l'invio (OAuth2 Gmail + SMTP). Ripetere su entrambi i progetti.

---

### 🟡 6 — Fallback JWT_SECRET hardcoded

**File:** [backend/src/middleware/auth.js:6](backend/src/middleware/auth.js#L6) e [authController.js:14](backend/src/controllers/authController.js#L14).

```js
const JWT_SECRET = process.env.JWT_SECRET || 'evoaz-dev-fallback-NOT-FOR-PRODUCTION'
```

`authController` fa `process.exit(1)` se manca `JWT_SECRET` **ma solo quando `NODE_ENV === 'production'`**. Se il processo gira senza `NODE_ENV=production` (facile da dimenticare in PM2), il sistema accetta token firmati con un secret **noto e pubblico nel codice**, permettendo la forgiatura di JWT `type:'full'` arbitrari → bypass totale dell'autenticazione admin.

**Rimedio:** far fallire l'avvio se `JWT_SECRET` manca **in qualsiasi ambiente** (rimuovere la condizione su `NODE_ENV`), e assicurarsi che `NODE_ENV=production` sia impostato in `ecosystem.config.cjs`.

---

### 🟡 7 — Nessuna autorizzazione per ruolo

Tutte le rotte admin sono protette da `authMiddleware` (autenticazione), ma **non esiste concetto di ruolo/permesso**: qualsiasi utente autenticato può creare/eliminare altri utenti admin, leggere tutte le candidature con dati sensibili, gestire contratti, ecc. La tabella `users` non ha una colonna `role`.

Non è una vulnerabilità sfruttabile da esterni, ma è un rischio di *privilege* interno: ogni account = super-admin. Se in futuro si aggiungono utenti "operatori", serviranno controlli di autorizzazione.

**Rimedio:** valutare un campo `role` + middleware `requireRole('admin')` sulle rotte sensibili (gestione utenti, retention, settings).

---

### 🔵 8 — Debug HMAC può loggare payload webhook in chiaro

**File:** [backend/src/services/docuseal.service.js:326](backend/src/services/docuseal.service.js#L326) (solo EvoAz).

Con `DOCUSEAL_HMAC_DEBUG=true` il body del webhook viene loggato in chiaro (`console.log(body.toString())`), potendo finire dati contrattuali/PII nei log. Attualmente il flag è `false` in `.env` — ok, ma è un piede di porco pronto. **Rimedio:** rimuovere il log del body completo o troncarlo, e assicurarsi che il flag resti `false` in produzione.

---

### 🔵 9 — Anagrafica sensibile esposta solo dietro token

`getOnboardingForm` restituisce CF, IBAN e dati documento a chiunque conosca il token (32 byte random — robusto). Il token è l'unico segreto. Combinato con il finding #2 (riutilizzabilità), il rischio sale. Il token è forte, ma valuta di **non esporre l'IBAN in lettura** nel form pre-compilato pubblico (mostrare solo gli ultimi 4 caratteri).

---

### 🔵 10 — Permessi file dei segreti

`backend/.env` e `backend/src/google-oauth2-credentials.json` hanno permessi `-rw-r--r--` (644, leggibili da tutti gli utenti del sistema). **Rimedio:** `chmod 600` e verificare l'ownership del processo Node.

---

## Cosa è fatto bene ✅

Da segnalare, perché il livello di base è discreto:

- **Query SQL sempre parametrizzate** (`?` + array), incluse le `IN (...)` costruite dinamicamente → nessuna SQL injection rilevata.
- **Password** con `bcrypt` (cost 12) e confronto costante.
- **2FA TOTP** implementata bene (setup, conferma, disable, reset admin).
- **Logout server-side** reale via `last_logout_at` verificato nel middleware.
- **Upload**: MIME allow-list, limiti di dimensione, filename randomizzati (`crypto.randomBytes`) — no path traversal, no overwrite.
- **Honeypot anti-spam** nel form candidatura (`website`).
- **Webhook DocuSeal**: verifica HMAC-SHA256 (è la vera guardia), + allowlist IP informativa.
- **OAuth2 Google**: `state` anti-CSRF verificato; output HTML della callback con escaping degli errori.
- **CORS** con allow-list esplicita di origin.
- **`.gitignore`** copre correttamente `.env`, credenziali, uploads, backup-db, logs — il problema è il solo `appunti_minio.txt` sfuggito.
- Endpoint test formazione: token one-time (`used_at`), scadenza, e rimozione delle risposte corrette lato server (anti-copia).

---

## Priorità d'intervento

1. **Oggi:** ruotare le credenziali MinIO e purgare `appunti_minio.txt` dalla history (finding #1).
2. **Questa settimana:** fix onboarding `used_at` (#2), `crypto` per le password temp (#3), rate-limit form su EvoAz (#4), `JWT_SECRET` obbligatorio ovunque (#6).
3. **A seguire:** `npm audit fix` + upgrade nodemailer (#5), permessi file (#10), valutare ruoli (#7).

> Applicando le fix, mantenere i due progetti **allineati**: quasi tutti i finding valgono per entrambi. Il modulo contratti (finding #8) riguarda solo EvoAz finché Supernova non lo migra — a quel punto rivedere anche lì.
