Skip to content

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àRilevamentoSoluzione
1AltaI 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()).
2AltaLa 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.
3MediaL'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%).
4MediaIl 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.
7BassaGli 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.
8BassaI 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).
6BassaIl 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àRilevamentoRaccomandazione
5MediaIl 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.
9InfoLa 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.
10InfoLa 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.

Rilasciato sotto licenza MIT.