Revisione di Sicurezza — Flusso dei Pagamenti
Ambito: src/server/routes/api/payments.ts (checkout Stripe, webhook, verifica on-chain, download riservati). Revisionato il 12 Giugno 2026.
Risolti in questa revisione
| N. | Gravità | Rilevamento | Soluzione |
|---|---|---|---|
| 1 | Alta | I codici di sblocco venivano generati tramite Math.random() (nei flussi di acquisto, abbonamento e webhook). L'algoritmo PRNG di V8 è prevedibile: un attaccante che osserva una serie di codici può ricostruirne lo stato e derivare altri codici validi, sbloccando contenuti a pagamento senza effettuare transazioni. | Tutti i codici vengono ora generati tramite crypto.randomBytes (generateUnlockCode()). |
| 2 | Alta | La variabile feeTxHash (transazione della quota per l'etichetta nei pagamenti diretti ripartiti) non aveva protezione da replay: un singolo pagamento della quota alla tesoreria poteva coprire infiniti acquisti. La transazione di acquisto txHash era protetta, quella della quota no. | La transazione della quota viene ora controllata rispetto alla stessa tabella degli hash utilizzati prima della verifica ed è "bruciata" inserendo una riga contrassegnata con FEE- a sblocco avvenuto. Le righe marcatore non contengono ID di tracce/release/asset, pertanto non possono essere spese come codici di download. |
| 3 | Media | L'importo della quota dell'etichetta non veniva verificato — si controllava solo che la transazione della quota avesse come destinatario la tesoreria e fosse andata a buon fine. Un acquirente poteva inviare 1 wei come "quota". | L'importo della quota viene ora verificato rispetto a prezzo effettivo × adminFeePct sia per le quote native in ETH (feeTx.value, con tolleranza del 5% per la fluttuazione del tasso di cambio) che per le quote in USDC (estratto dai calldata ERC-20, tolleranza 1%). |
| 4 | Media | Il percorso /verify verificava il prezzo confrontandolo solo con track.price, mentre il percorso Stripe consulta anche i prezzi specifici per release (release_tracks). Una traccia venduta a un prezzo maggiore all'interno di una release poteva essere sbloccata on-chain pagando il prezzo (inferiore) impostato a livello di singola traccia. | La rotta /verify ora calcola il prezzo effettivo (price, price_usdc, currency) tramite getTrackPriceFromRelease esattamente come il percorso Stripe, ed esegue il confronto con tale valore in tutti e tre i casi di verifica. |
| 7 | Bassa | Gli URL di successo/annullamento (successUrl/cancelUrl) per le sessioni Stripe venivano accettati dal client senza alcuna convalida — un link opportunamente modificato poteva reindirizzare l'utente a un URL malevolo a checkout completato (vettore di phishing, nessun fondo a rischio). | Entrambi gli URL devono ora corrispondere all'origine dell'istanza (impostazione publicUrl o host della richiesta) su entrambe le rotte di creazione della sessione. |
| 8 | Bassa | I percorsi /verify e /subscription/verify non richiedono autenticazione e ogni chiamata avvia due ricerche RPC — un facile bersaglio di amplificazione per esaurire le quote di richieste RPC. | Introdotto un rate limiter dedicato su entrambe le rotte: 30 richieste ogni 15 minuti per IP (il limitatore globale è di 1000 richieste ogni 15 minuti). |
| 6 | Bassa | Il JWT di sessione veniva accettato sia come parametro di query (?token=) sia nel corpo della richiesta. I token negli URL finiscono nei log del server, nei proxy e nella cronologia del browser; un link di download trapelato equivaleva a una sessione trapelata. | I token di sessione sono ora accettati esclusivamente nell'intestazione (header) HTTP. I percorsi di download accettano ora un token con validità limitata allo scopo (?dt=, scadenza a 5 minuti) coniato tramite POST /api/payments/download-token; i token di download sono rifiutati in qualsiasi altra rotta autenticata, per cui un link trapelato scade in pochi minuti e non concede accessi oltre il download specifico. |
Rilevamenti aperti (accettati o da approfondire)
| N. | Gravità | Rilevamento | Raccomandazione |
|---|---|---|---|
| 5 | Media | Il metodo purchaseWithUSDC tramite il contratto di checkout si affida esclusivamente alla corrispondenza del trackId — nessun controllo dell'importo lato server. Questo è sicuro solo se il contratto distribuito applica la propria associazione di prezzi; il server non può sapere se l'indirizzo web3_checkout_address configurato lo faccia effettivamente. | Questa assunzione di fiducia è ora documentata qui e in STATUS.md; facoltativamente, è possibile leggere l'associazione dei prezzi del contratto tramite chiamata RPC e confrontarla. Poiché solo l'amministratore dell'istanza può impostare web3_checkout_address, lo sfruttamento di questa vulnerabilità richiede un contratto distribuito dall'amministratore che sia malevolo o contenga bug. |
| 9 | Info | La gestione dei percorsi nei download è sicura: il valore track.file_path proviene dal database (sotto il controllo dello scansionatore), non dalla richiesta; i percorsi assoluti degli asset sono impostati dall'amministratore. | — |
| 10 | Info | La verifica della firma del webhook di Stripe è implementata correttamente leggendo il corpo grezzo (raw body) prima di qualsiasi parser JSON. | — |
Note sul modello di fiducia
- I percorsi di "pagamento diretto" on-chain (B/C) verificano il destinatario e l'importo ma non il mittente: chiunque indichi una transazione idonea (es. individuata su un block explorer) può richiedere il codice di sblocco prima dell'acquirente reale, poiché il codice viene restituito a chi invia per primo l'hash della transazione. Questa è una caratteristica intrinseca degli schemi basati sulla presentazione dell'hash; la tabella dei replay garantisce quantomeno una sola richiesta per transazione. Una richiesta di firma di un messaggio (in cui l'acquirente dimostra il controllo dell'indirizzo mittente) risolverebbe il problema.
- Un'istanza a singolo artista self-hosted (artista = amministratore = tesoreria) non risente dei rilevamenti da 3 a 5, che sono rilevanti solo in contesti multi-tenant gestiti da etichette con più artisti.