Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s
Deploy stack su Proxmox / deploy (push) Failing after 3s
Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA), apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/; infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e workflow Gitea Actions per il deploy via SSH a ogni push su master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# Security review pre go-live (prompt 34)
|
||||
|
||||
Data: 2026-10-05. Ambito: `apps/api`, `apps/public`, `apps/private`, `medusa/apps/backend`, `infra/` e `docker-compose.prod.yml`.
|
||||
Metodo: lettura del codice e della configurazione, `npm audit`, test automatici, verifica nel browser (build di produzione del sito, area riservata servita da Caddy con la CSP di produzione).
|
||||
|
||||
Esiti: **ok** = conforme, nessuna modifica · **corretto** = trovato un problema e risolto in questa review · **da fare** = rimane aperto, con motivazione.
|
||||
|
||||
## Riepilogo
|
||||
|
||||
| # | Voce | Esito |
|
||||
|---|---|---|
|
||||
| 1 | Autenticazione, ruoli e ownership delle route `private/` e `admin/` | corretto |
|
||||
| 2 | Input validati da schema, nessuna SQL concatenata | ok |
|
||||
| 3 | Rate limit su auth, form pubblici, configuratore, upload | corretto |
|
||||
| 4 | CORS, cookie, nessun token in localStorage | corretto |
|
||||
| 5 | Upload: whitelist, dimensione, nomi rigenerati, path traversal | ok |
|
||||
| 6 | HTML del blog sanitizzato, nessun `innerHTML`/`bypassSecurityTrust*` su contenuti non sanitizzati | ok |
|
||||
| 7 | CSP senza `'unsafe-inline'` negli script (rimandata dal prompt 33) | corretto |
|
||||
| 8 | Webhook Medusa e Stripe: firma e idempotenza | ok |
|
||||
| 9 | Segreti fuori dal repo e dai log; `npm audit` senza alte/critiche | corretto / da fare |
|
||||
| 10 | Errori in produzione senza stack; log senza password, token, carte | ok |
|
||||
| 11 | Cookie banner: nessun cookie non tecnico prima del consenso | ok |
|
||||
| 12 | Database: utente applicativo non superuser, utenti distinti per app e Medusa | ok |
|
||||
|
||||
## 1. Autenticazione, ruoli e ownership — corretto
|
||||
|
||||
- **Area admin**: tutte le route `/admin/*` stanno dietro un unico hook (`routes/admin/index.ts`). Prima controllava solo il ruolo nel JWT, quindi un admin disabilitato o declassato manteneva l'accesso fino alla scadenza del token (≤ 15 min).
|
||||
**Corretto**: un secondo hook rilegge l'utente dal DB (`requireActiveUser`) e richiede ancora `role = admin`. Disabilitato → 401, declassato → 403, subito. Test: `admin.test.ts` → "admin disabilitato o declassato".
|
||||
- **Route private** (`/me*`, `/installer/profile`): `authenticate` / `requireRole("installer")` più `requireActiveUser` in ogni handler, che rilegge l'utente (disabilitato → 401). Verificato su tutti i file di `routes/private/`.
|
||||
- **Ownership**: preventivi, ticket e messaggi filtrano sempre su `user_id` dell'utente corrente (`mine()`, `loadOwn()`). Risorsa altrui, in bozza o inesistente → 404 identico. Gli ordini Medusa sono filtrati sul customer dell'utente e ricontrollati sul dettaglio (`order.customer_id`). I carrelli sono verificati da `resolveCart`. I documenti seguono `visibleLevels`, riletti dal DB a ogni richiesta. Coperto dai test di `quotes`, `tickets`, `account`, `shop`, `documents`.
|
||||
- **Ruolo admin non assegnabile** dalla registrazione (enum di `accountType`); JWT HS256 con algoritmo fissato in verifica.
|
||||
- **Anti-enumerazione alla registrazione**: con un'email già registrata la risposta era 202 come le altre, ma più veloce (nessun hash argon2), quindi i tempi rivelavano l'email.
|
||||
**Corretto**: anche in quel caso si calcola l'hash (`auth.ts`). Il login usava già un hash fittizio.
|
||||
- Medusa: le route `/internal/*` richiedono `x-internal-secret` (confronto `timingSafeEqual` su digest SHA-256); il dashboard ha whitelist IP e basic auth in Caddy.
|
||||
|
||||
## 2. Validazione e SQL — ok
|
||||
|
||||
- Ogni route Fastify dichiara gli schemi zod di body, query e params (`fastify-type-provider-zod`), più lo schema di risposta. Le route interne di Medusa validano con `validateAndTransformBody`.
|
||||
- Nessun SQL concatenato: Drizzle parametrizza sempre. Nessun uso di `sql.raw`, `.unsafe()` o `execute` con stringhe composte. Le interpolazioni nei template `sql\`…\`` sono parametri (`tsQuery`, advisory lock). Le ricerche `ilike` fanno l'escape di `%`, `_` e `\` (`likeEscape`). Lo stesso vale in Medusa (claim con ILIKE letterale).
|
||||
|
||||
## 3. Rate limit — corretto
|
||||
|
||||
| Route | Limite per IP |
|
||||
|---|---|
|
||||
| globale | 300/min |
|
||||
| login, reset password, verifica email, cambio password, cancellazione account | 5/min |
|
||||
| registrazione, password dimenticata, reinvio verifica | 3 ogni 15 min |
|
||||
| refresh | 30/min |
|
||||
| `POST /contact`, `POST /showroom/bookings`, `POST /configurator/submit` | 5 ogni 15 min |
|
||||
| `POST /configurator/evaluate`, `GET /forms/token` | 60/min |
|
||||
| ticket: nuovo / messaggio | 10/ora, 30/ora |
|
||||
| pagamento e completamento ordine | 20/min |
|
||||
|
||||
**Corretto**:
|
||||
- **Upload admin** (`POST /admin/documents`, `/admin/uploads/image`, `/admin/quotes/:id/pdf`): avevano solo il limite globale. Ora 30/min (`UPLOAD_RATE_LIMIT`).
|
||||
- **`POST /shop/cart/address`**: crea o cerca un customer guest in Medusa per ogni email e registra consensi. Ora 20/min.
|
||||
- **IP reale dietro due proxy**: con `TRUST_PROXY=true` e il server SSR che chiama l'API su `http://api:3000`, tutte le richieste fatte durante il rendering arrivavano dall'IP del container del sito. Così condividevano un unico limite di 300/min: con un po' di traffico il sito avrebbe ricevuto 429 dall'API.
|
||||
- L'API ora accetta in `TRUST_PROXY` un elenco di proxy fidati. In produzione vale `loopback,uniquelocal`, cioè si fida di `X-Forwarded-For` solo dai container della rete Docker. Un client diretto non può falsificarlo, e Caddy sostituisce l'intestazione dei client.
|
||||
- Il server SSR inoltra l'`X-Forwarded-For` ricevuto da Caddy (`forwarded-for.interceptor.ts`), così ogni visitatore ha il suo limite.
|
||||
- Test: `app.test.ts` (IP da proxy fidato e da client diretto).
|
||||
- Nota: il contatore del rate limit è in memoria, per processo. Va bene con una sola istanza dell'API; con più istanze serve lo store Redis di `@fastify/rate-limit`.
|
||||
|
||||
## 4. CORS, cookie, storage — corretto
|
||||
|
||||
- CORS: solo `CORS_ORIGINS` (sito e area riservata) con credenziali; il webhook ha `cors: false`.
|
||||
- Cookie:
|
||||
|
||||
| Cookie | Attributi |
|
||||
|---|---|
|
||||
| `iti_rt` | `HttpOnly`, `Secure`, `SameSite=Lax`, `Path=/auth` |
|
||||
| `cart_id` | firmato, `HttpOnly`, `Secure`, `SameSite=Lax` |
|
||||
| `iti_session` | non httpOnly (voluto: solo un indizio, valore `1`, nessun segreto) |
|
||||
| `iti_consent` | non httpOnly (voluto: lo legge il banner) |
|
||||
|
||||
- Token: access token solo in memoria in entrambe le app. In `localStorage` e `sessionStorage` ci sono solo configuratore, ultimo ordine e riepilogo dell'invio, senza token (verificato con grep e nel browser).
|
||||
- **Corretto**: in produzione l'API ora **rifiuta di partire** con:
|
||||
- `AUTH_COOKIE_SECURE=false`;
|
||||
- origini CORS non `https://`;
|
||||
- `EMAIL_OUTBOX_DIR` impostata, che salverebbe su disco le email complete con i link di reset e di verifica, cioè token validi.
|
||||
|
||||
Test in `app.test.ts`.
|
||||
|
||||
## 5. Upload — ok
|
||||
|
||||
- Whitelist per estensione **e** magic bytes **e** MIME dichiarato (SVG e HTML mai ammessi), `UPLOAD_MAX_BYTES` con conteggio in streaming (413) e cancellazione del file parziale.
|
||||
- Chiavi generate dal server (`<uuid><ext>`), doppio controllo anti path traversal, scrittura con `wx`.
|
||||
- `private/` mai servita staticamente: Caddy monta solo `uploads/public`. `media.<dominio>` ha `CSP sandbox` e `nosniff`.
|
||||
- Coperto da `documents.test.ts` (14 casi) e dalla verifica del prompt 33.
|
||||
|
||||
## 6. HTML del blog e frontend — ok
|
||||
|
||||
- L'API converte il markdown con `marked` e poi lo filtra con `sanitize-html` (whitelist; niente `script`, `iframe`, `style`, `on*`, `javascript:`, `data:`).
|
||||
- Nei frontend ci sono solo due `bypassSecurityTrustHtml`, entrambi su HTML che arriva da quel sanitizer: articolo del blog (`blog-post.ts`) e anteprima dell'editor (`blog-editor.ts`).
|
||||
- Nessun altro `innerHTML`, `outerHTML` o `insertAdjacentHTML`.
|
||||
- Verificato nel browser: markdown con `<img onerror>` e `<script>` nell'anteprima → tag rimossi, nessun codice eseguito.
|
||||
|
||||
## 7. Content-Security-Policy — corretto
|
||||
|
||||
Era il punto rimandato dal prompt 33: `'unsafe-inline'` negli script di entrambe le app.
|
||||
|
||||
**Sito (SSR) — nonce per risposta** (`apps/public/src/server/csp.ts`):
|
||||
- `src/index.html` ha `ngCspNonce="__CSP_NONCE__"`. La build mette quel segnaposto come `nonce` sugli script e sugli stili inline e lo passa a beasties (CSS critici). Durante l'SSR, `CSP_NONCE` di Angular legge lo stesso attributo per lo script di avvio dell'event replay.
|
||||
- Il server Express sostituisce il segnaposto con 128 bit casuali per ogni risposta HTML e invia l'intestazione CSP: `script-src 'self' 'nonce-…' https://js.stripe.com https://*.js.stripe.com`, `connect-src` sull'API e Stripe, `img-src` su `PUBLIC_MEDIA_URL` (nuova variabile) e tile OSM, `frame-ancestors 'none'`.
|
||||
- Con `ng serve` la CSP non viene inviata.
|
||||
- Caddy non sovrascrive più la CSP del sito. Imposta solo un default restrittivo (`?Content-Security-Policy`) per le risposte che non ce l'hanno: statici ed errori. Verificato con Caddy reale: la CSP con nonce passa invariata, gli statici ricevono il default.
|
||||
|
||||
**Area riservata** (statica): `inlineCritical: false` nella build di produzione, quindi l'`index.html` non contiene più script inline. CSP `script-src 'self'`.
|
||||
|
||||
**Verificato nel browser**:
|
||||
- sito (build di produzione + API e Medusa reali): idratazione, stylesheet attivato dallo script di beasties con il nonce, aggiunta al carrello funzionante; uno script inline iniettato viene **bloccato** (`script-src-elem`);
|
||||
- area riservata servita da Caddy con la CSP di produzione: login, dashboard admin ed editor del blog senza violazioni.
|
||||
|
||||
`style-src 'unsafe-inline'` resta: servono gli stili dei componenti Angular e gli attributi `style` di Leaflet. Il rischio è basso, perché gli script restano bloccati.
|
||||
|
||||
## 8. Webhook — ok
|
||||
|
||||
- **Medusa → Fastify**: HMAC-SHA256 sul body grezzo verificato prima del parse, con `timingSafeEqual`. Idempotenza su `webhook_events` (id UUID v5 deterministico, lock `FOR UPDATE`). Coperto da `webhooks.test.ts` (13 casi).
|
||||
- **Stripe → Medusa**: la firma la verifica il provider ufficiale `payment-stripe` (`webhookSecret`). Caddy espone solo `/hooks/payment/stripe_stripe`. L'elaborazione di Medusa è idempotente sullo stato della payment session (un evento ripetuto non cattura due volte).
|
||||
- **Corretto**: in produzione Medusa ora avvisa all'avvio se c'è `STRIPE_API_KEY` ma manca `STRIPE_WEBHOOK_SECRET`.
|
||||
- **Da fare**: il flusso Stripe reale (pagamento, 3DS, webhook firmato) non è ancora verificato end-to-end. Servono le chiavi di test (vedi prompt 13 e 26).
|
||||
|
||||
## 9. Segreti e dipendenze — corretto / da fare
|
||||
|
||||
- **Repo**: i file `.env*` (tranne gli `.example`), `*.private.env.json`, `uploads/`, `backups/` e `.outbox/` sono in `.gitignore`. Una ricerca di chiavi Stripe, Brevo, Resend e PEM tra i file che finirebbero nel repo non trova nulla; gli `.example` hanno solo segnaposto.
|
||||
**Corretto**: aggiunto `*.stackdump` (dump dei crash di Git Bash, due file non tracciati nel working tree).
|
||||
- **Log**: redatti `authorization`, `cookie`, `set-cookie`, `x-internal-secret`, `x-signature`. I body non vengono mai loggati. I token viaggiano solo nel body o nei cookie, mai negli URL dell'API. Il trasporto email `console` logga solo destinatario, template e oggetto.
|
||||
- **Corretto**: in produzione `medusa-config.ts` ora rifiuta di partire se `JWT_SECRET`, `COOKIE_SECRET`, `INTERNAL_API_SECRET` o `WEBHOOK_SECRET` mancano o hanno meno di 32 caratteri. Senza `JWT_SECRET`, Medusa userebbe il default `supersecret`.
|
||||
- **`npm audit` del monorepo**: 0 vulnerabilità nelle dipendenze di produzione. Ci sono 4 *moderate* solo di sviluppo (`esbuild` dentro `drizzle-kit`, server di sviluppo mai avviato; il fix proposto è un downgrade).
|
||||
- **`npm audit` di Medusa**: prima 110 *high*, tutte transitive dal pacchetto `@medusajs/*`, da due sole cause.
|
||||
- **lodash ≤ 4.17.23** (prototype pollution in `_.unset`/`_.omit`, code injection in `_.template`). **Corretto**: `overrides.lodash = ^4.18.1` in `medusa/package.json` e lockfile aggiornato. Una sola copia, 4.18.1; Medusa si avvia e interroga il DB (`medusa exec` verificato).
|
||||
- **braces ≤ 3.0.3** (stack exhaustion con pattern glob annidati). **Da fare**: non esiste una versione corretta (3.0.3 è l'ultima). Il pacchetto arriva da `fast-glob`/`micromatch` (caricamento dei moduli all'avvio) e dal bundler del dashboard (build). Non riceve mai pattern da input esterni, quindi non è sfruttabile. Le 77 *high* di produzione che restano sono tutte questa catena. Da ricontrollare a ogni aggiornamento di Medusa.
|
||||
|
||||
## 10. Errori e log in produzione — ok
|
||||
|
||||
- Mai stack trace nelle risposte. I 5xx rispondono `"Errore interno"` in produzione (test `app.test.ts`); gli errori di Medusa diventano 503/502 generici, con il dettaglio solo nei log.
|
||||
- Password: solo hash argon2id. Token: solo SHA-256 in DB (test `auth.test.ts`: nessun segreto nel dump del DB né nei log catturati).
|
||||
- Dati carta: non passano mai dai server, perché il Payment Element di Stripe li invia direttamente a Stripe.
|
||||
|
||||
## 11. Cookie banner — ok
|
||||
|
||||
Verificato nel prompt 28 e invariato:
|
||||
- alla prima visita nessun cookie e nessuna risorsa di terzi;
|
||||
- `iti_consent` viene scritto solo dopo la scelta;
|
||||
- i cookie di sessione e carrello sono tecnici, e nascono solo con login o aggiunta al carrello;
|
||||
- Stripe si carica solo al passo di pagamento (necessario al servizio richiesto);
|
||||
- le tile OSM si caricano solo dopo il clic su "Mostra mappa".
|
||||
|
||||
Oggi il sito non ha script di statistica o marketing; `ConsentService` è il punto da usare per quelli futuri.
|
||||
|
||||
## 12. Database — ok
|
||||
|
||||
- `iti_app` e `iti_medusa` sono utenti distinti. Nessuno dei due è superuser e non hanno `CREATEROLE`, `CREATEDB` né `BYPASSRLS` (verificato su `pg_roles`).
|
||||
- Ciascuno è proprietario solo del proprio database; `CONNECT` è revocato a `PUBLIC`.
|
||||
- Le estensioni le crea l'init del container (superuser), non le migrazioni.
|
||||
- Postgres e Redis stanno sulla rete Docker `internal`, senza porte pubblicate.
|
||||
- **Da fare** (minore): l'init crea anche `iti_app_test` in produzione, dove non serve. Dopo il primo deploy si può eliminare con `DROP DATABASE iti_app_test`.
|
||||
|
||||
## Punti rimasti aperti
|
||||
|
||||
1. **Stripe end-to-end** (pagamento, 3DS, carta rifiutata, webhook firmato): non verificato, mancano le chiavi di test (prompt 13 e 26).
|
||||
2. **braces** nelle dipendenze di Medusa: nessuna correzione upstream, non sfruttabile; ricontrollare agli aggiornamenti di Medusa.
|
||||
3. **Rate limit in memoria**: con più istanze dell'API, passare allo store Redis.
|
||||
4. **HTTPS reale e HSTS**: certificati Let's Encrypt e DNS da verificare sulla VPS. HSTS è senza `includeSubDomains`/`preload`, una scelta voluta (prompt 33).
|
||||
5. **Immagini esterne negli articoli del blog**: la CSP le blocca (`img-src` limitato a self, media, OSM e Stripe). Le immagini vanno caricate dall'editor.
|
||||
6. **GDPR**: la cancellazione effettiva dei dati dopo la richiesta dell'utente è manuale; privacy e cookie policy sono da validare con il consulente.
|
||||
7. **`iti_app_test` in produzione**: da eliminare dopo il primo deploy.
|
||||
8. **Utenti del dashboard Medusa**: password robuste e whitelist IP (`SHOP_ADMIN_ALLOWED_IPS`) sull'ufficio o sulla VPN; il default ammette tutti, protetti solo dalla basic auth.
|
||||
Reference in New Issue
Block a user