Gli upload di file sicuri richiedono permessi restrittivi, limiti di dimensione, URL firmati e semplici pattern di scansione per evitare incidenti.

Gli upload sembrano innocui: una foto profilo, un PDF, un foglio di calcolo. Ma sono spesso il primo incidente di sicurezza perché permettono a estranei di dare al tuo sistema una scatola misteriosa. Se la accetti, la conservi e la mostri ad altri, hai creato un nuovo modo di attaccare la tua app.
Il rischio non è solo “qualcuno carica un virus.” Un upload dannoso può far uscire file privati, far lievitare la bolletta dello storage o ingannare gli utenti per ottenere accessi. Un file chiamato “invoice.pdf” potrebbe non essere affatto un PDF. Anche PDF e immagini reali possono causare problemi se la tua app si fida dei metadata, genera preview automaticamente o li serve con regole sbagliate.
I fallimenti reali tendono a sembrare così:
Un dettaglio guida molti incidenti: conservare file non è la stessa cosa che servirli. Lo storage è dove tieni i byte. Il serving è come quei byte vengono consegnati ai browser e alle app. Le cose vanno male quando un’app serve upload utente con lo stesso livello di fiducia e le stesse regole del sito principale, così il browser tratta l’upload come “attendibile”.
“Abbastanza sicuro” per una piccola o media app di solito significa poter rispondere a quattro domande senza ammettere incertezza: chi può caricare, cosa accetti, quanto è grande e quante volte, e chi può leggerlo dopo. Anche se costruisci in fretta (con codice generato o una piattaforma guidata da chat), quelle protezioni contano ancora.
Tratta ogni upload come input non attendibile. Il modo pragmatico per mantenere gli upload sicuri è immaginare chi può abusarne e cosa significa per loro “riuscire”.
La maggior parte degli attaccanti sono bot che scannerizzano form di upload deboli o utenti reali che spingono i limiti per ottenere storage gratuito, raschiare dati o trollare il servizio. A volte è un concorrente che testa fughe di dati o interruzioni.
Cosa vogliono? Di solito uno di questi risultati:
Poi mappa i punti deboli. L’endpoint di upload è la porta d’ingresso (file troppo grandi, formati strani, alto tasso di richieste). Lo storage è il retrobottega (bucket pubblici, permessi sbagliati, cartelle condivise). Gli URL di download sono l’uscita (prevedibili, a lunga durata o non legati a un utente).
Esempio: una funzione di “upload CV”. Un bot carica migliaia di PDF grandi per aumentare i costi, mentre un utente abusivo carica un file HTML e lo condivide come “documento” per ingannare altri.
Prima di aggiungere controlli, decidi cosa conta di più per la tua app: privacy (chi può leggere), disponibilità (puoi continuare a servire), costo (storage e banda) e compliance (dove i dati sono conservati e per quanto). Quella lista di priorità mantiene coerenti le decisioni.
La maggior parte degli incidenti sugli upload non sono hack sofisticati. Sono bug semplici “posso vedere il file di qualcun altro”. Tratta i permessi come parte integrante degli upload, non come una cosa da aggiungere dopo.
Inizia con una regola: default deny. Considera ogni oggetto caricato come privato finché non permetti esplicitamente l’accesso. “Privato per default” è un solido baselines per fatture, file medici, documenti di conto e tutto ciò legato a un utente. Rendi i file pubblici solo quando l’utente se lo aspetta chiaramente (per esempio un avatar pubblico), e anche in quel caso considera accessi a tempo limitato.
Mantieni ruoli semplici e separati. Una suddivisione comune è:
Non fare affidamento su regole a livello di cartella come “tutto in /user-uploads/ va bene.” Controlla proprietà o accesso tenant al momento della lettura, per ogni file. Questo ti protegge quando qualcuno cambia team, lascia un’organizzazione o un file viene riassegnato.
Un buon pattern per il supporto è stretto e temporaneo: concedi accesso a un file specifico, logga l’azione e fai scadere l’accesso automaticamente.
La maggior parte degli attacchi agli upload inizia con un trucco semplice: un file che sembra sicuro per via del nome o di un header del browser, ma in realtà è altro. Tratta tutto quello che arriva dal client come non attendibile.
Inizia con un allowlist: decidi i formati esatti che accetti (per esempio .jpg, .png, .pdf) e rifiuta tutto il resto. Evita “qualsiasi immagine” o “qualsiasi documento” a meno che non sia veramente necessario.
Non fidarti dell’estensione del filename o dell’header Content-Type dal client. Entrambi sono facili da falsificare. Un file chiamato invoice.pdf può essere un eseguibile e Content-Type: image/png può essere falso.
Un approccio più solido è ispezionare i primi byte del file, spesso chiamati “magic bytes” o firma del file. Molti formati comuni hanno header coerenti (come PNG e JPEG). Se l’header non corrisponde a quanto accetti, rifiuta il file.
Una configurazione pratica di validazione:
Rinominare è più importante di quanto sembri. Se memorizzi nomi forniti dall’utente direttamente, inviti a trucchi di path, caratteri strani e sovrascritture accidentali. Usa un ID generato per lo storage e conserva il filename originale solo per la visualizzazione.
Per le foto profilo, accetta solo JPEG e PNG, verifica gli header e rimuovi i metadata se possibile. Per i documenti, valuta di limitare ai PDF e rifiutare qualunque contenuto attivo. Se in seguito decidi di permettere SVG o HTML, trattali come potenzialmente eseguibili e isolali.
La maggior parte delle interruzioni di upload non sono “trucchi sofisticati”. Sono file enormi, troppe richieste o connessioni lente che occupano i server finché l’app non sembra giù. Tratta ogni byte come un costo.
Scegli una dimensione massima per feature, non un numero globale. Un avatar non ha bisogno dello stesso limite di un documento fiscale o di un video breve. Imposta il limite più piccolo che sembri normale, poi aggiungi un percorso separato per i “carichi grandi” solo quando serve davvero.
Applica i limiti in più punti, perché i client possono mentire: nella logica dell’app, nel web server o reverse proxy, con timeout di upload e con rifiuto anticipato quando la dimensione dichiarata è troppo grande (prima di leggere l’intero body).
Esempio concreto: avatar limitati a 2 MB, PDF a 20 MB e tutto ciò di più richiede un percorso diverso (come upload diretto su object storage con URL firmato).
Anche file piccoli possono diventare DoS se qualcuno li carica in loop. Aggiungi rate limit agli endpoint di upload per utente e per IP. Considera limiti più stringenti per traffico anonimo rispetto a utenti autenticati.
Gli upload resumabili aiutano gli utenti reali con reti scadenti, ma il token di sessione deve essere stretto: scadenza breve, legato all’utente e vincolato a una dimensione e destinazione specifica. Altrimenti gli endpoint di “resume” diventano un tubo gratuito verso il tuo storage.
Quando blocchi un upload, restituisci errori chiari all’utente (file troppo grande, troppe richieste) ma non rivelare dettagli interni (stack trace, nomi di bucket, informazioni sui vendor).
Gli upload sicuri non riguardano solo cosa accetti. Riguardano anche dove va il file e come lo restituisci dopo.
Tieni i byte degli upload fuori dal tuo database principale. La maggior parte delle app ha solo bisogno di metadata nel DB (owner user ID, filename originale, tipo rilevato, dimensione, checksum, storage key, created time). Conserva i byte in object storage o un servizio file pensato per blob grandi.
Separa i file pubblici da quelli privati a livello di storage. Usa bucket o container differenti con regole diverse. I file pubblici (come avatar pubblici) possono essere leggibili senza login. I file privati (contratti, fatture, documenti medici) non dovrebbero mai essere leggibili pubblicamente, anche se qualcuno indovina l’URL.
Evita di servire i file utente dallo stesso dominio della tua app quando possibile. Se scivola un file rischioso (HTML, SVG con script o stranezze di MIME sniffing del browser), ospitarlo sul dominio principale può trasformarlo in un takeover di account. Un dominio di download dedicato (o dominio di storage) limita la blast radius.
Al download, forza header sicuri. Imposta un Content-Type prevedibile basato su ciò che permetti, non su ciò che dichiara l’utente. Per qualsiasi cosa che il browser potrebbe interpretare, preferisci inviarla come download.
Alcuni default che prevengono sorprese:
Content-Disposition: attachment per i documenti.Content-Type sicuro (o application/octet-stream).La retention è sicurezza, anche quella. Elimina upload abbandonati, rimuovi versioni vecchie dopo la sostituzione e imposta limiti temporali per i file temporanei. Meno dati conservati = meno cose da perdere.
Gli URL firmati (spesso chiamati pre-signed URLs) sono un modo comune per permettere agli utenti di caricare o scaricare file senza rendere pubblico il bucket e senza mandare ogni byte attraverso la tua API. L’URL porta permesso temporaneo, poi scade.
Due flussi comuni:
Il direct-to-storage riduce il carico sull’API, ma rende più importanti le regole di storage e i vincoli dell’URL.
Tratta un URL firmato come una chiave monouso. Fallo specifico e a breve scadenza.
Un pattern pratico è creare prima un record di upload (status: pending), poi emettere l’URL firmato. Dopo l’upload, conferma che l’oggetto esista e corrisponda a dimensione e tipo attesi prima di marcarlo ready.
Un flusso sicuro è per lo più regole chiare e stato chiaro. Tratta ogni upload come non attendibile finché non superi i controlli.
Scrivi cosa permette ogni funzionalità. Una foto profilo e un documento fiscale non dovrebbero condividere gli stessi tipi di file, limiti di dimensione o visibilità.
Definisci i tipi consentiti e un limite di dimensione per feature (per esempio: foto fino a 5 MB; PDF fino a 20 MB). Applica le stesse regole nel backend.
Crea un “record upload” prima che arrivino i byte. Memorizza: owner (utente o org), purpose (avatar, fattura, allegato), filename originale, dimensione massima prevista e uno status come pending.
Carica in una posizione privata. Non lasciare che il client scelga il path finale.
Valida di nuovo server-side: dimensione, magic bytes/tipo, allowlist. Se passa, cambia lo status in uploaded.
Scansiona per malware e aggiorna lo status a clean o quarantined. Se la scansione è asincrona, mantieni l’accesso bloccato mentre aspetti.
Permetti download, preview o elaborazioni solo quando lo status è clean.
Esempio piccolo: per una foto profilo, crea un record legato all’utente e allo scopo avatar, conserva privatamente, conferma che sia davvero JPEG/PNG (non solo che abbia quel nome), scansiona e poi genera un URL di preview.
La scansione è una rete di sicurezza, non una garanzia. Serve a catturare file noti e trucchi ovvi, ma non rileverà tutto. L’obiettivo è semplice: ridurre il rischio e rendere i file sconosciuti innocui per default.
Un pattern affidabile è prima quarantena. Salva ogni nuovo upload in una locazione privata e contrassegnalo come pending. Solo dopo che supera i controlli spostalo in “clean” (o segnalo come disponibile).
Le scansioni sincrone funzionano solo per file piccoli e basso traffico perché l’utente aspetta. La maggior parte delle app scansiona in modo asincrono: accetta l’upload, restituisce uno stato “in elaborazione”, scansiona in background.
La scansione base è tipicamente un motore antivirus (o un servizio) più alcuni guardrail: scansione AV, controlli sul tipo di file (magic bytes), limiti sugli archivi (zip bomb, zip annidati, dimensione decompattata enorme) e blocco dei formati non necessari.
Se lo scanner fallisce, scade o restituisce “sconosciuto”, tratta il file come sospetto. Tienilo in quarantena e non fornire link di download. Qui le squadre si scottano: “scan failed” non dovrebbe mai diventare “spediscilo comunque”.
Quando blocchi un file, mantieni il messaggio neutro: “Non possiamo accettare questo file. Prova con un file diverso o contatta il supporto.” Non affermare di aver rilevato malware a meno che tu non sia certo.
Considera due feature: una foto profilo (mostrata pubblicamente) e una ricevuta PDF (privata, usata per billing o support). Entrambe sono problemi di upload, ma non dovrebbero condividere le stesse regole.
Per la foto profilo, mantienila stretta: solo JPEG/PNG, cap dimensione (per esempio 2–5 MB) e riconverti lato server così non servi i byte originali dell’utente. Conservala in storage pubblico solo dopo i controlli.
Per la ricevuta PDF, accetta dimensioni maggiori (es. fino a 20 MB), tienila privata per default e evita di renderla inline dal dominio principale dell’app.
Un semplice modello di stati tiene informati gli utenti senza esporre internals:
Gli URL firmati si adattano bene qui: usa un URL firmato a breve scadenza per l’upload (write-only, una sola object key). Emetti un altro URL firmato a breve scadenza per la lettura, e solo quando lo status è clean.
Logga ciò che serve per le indagini, non il file stesso: user ID, file ID, tipo stimato, dimensione, storage key, timestamp, risultato scansione, request ID. Evita di loggare contenuti raw o dati sensibili trovati dentro i documenti.
La maggior parte dei bug sugli upload nasce perché una scorciatoia “temporanea” diventa permanente. Assumi che ogni file sia non attendibile, ogni URL verrà condiviso e ogni impostazione “lo sistemiamo dopo” verrà dimenticata.
Le trappole ricorrenti:
Content-Type sbagliato, lasciando che il browser interpreti contenuti rischiosi.Il monitoring è ciò che le squadre saltano fino a quando la bolletta dello storage non esplode. Monitora volume upload, dimensione media, top uploader e tassi di errore. Un account compromesso può caricare migliaia di file grandi in una notte.
Esempio: un team salva avatar con nomi forniti dagli utenti come “avatar.png” in una cartella condivisa. Un utente sovrascrive le immagini di altri. La soluzione è noiosa ma efficace: genera object key server-side, tieni gli upload privati per default ed esponi un’immagine ridimensionata tramite una risposta controllata.
Usa questo come pass finale prima del rilascio. Tratta ogni voce come un blocker di release, perché la maggior parte degli incidenti deriva da una guardrail mancante.
Content-Type prevedibile, filename sicuri e attachment per i documenti.Scrivi le tue regole in linguaggio semplice: tipi consentiti, dimensioni massime, chi può accedere a cosa, quanto durano gli URL firmati e cosa significa “scan passed”. Questo diventa il contratto condiviso tra product, engineering e support.
Aggiungi alcuni test che catturino i fallimenti comuni: file sovradimensionati, eseguibili rinominati, letture non autorizzate, URL firmati scaduti e download con “scan pending”. Questi test costano poco rispetto a un incidente.
Se stai costruendo e iterando rapidamente, aiuta usare un workflow dove puoi pianificare i cambiamenti e tornare indietro in sicurezza. Le squadre che usano Koder.ai (koder.ai) spesso si affidano alla modalità di pianificazione e a snapshot/rollback mentre stringono le regole di upload nel tempo, ma il requisito centrale resta lo stesso: la policy la applica il backend, non l’interfaccia utente.
Inizia con privato per default e tratta ogni upload come input non attendibile. Applica quattro controlli base nel backend:
Se riesci a rispondere a queste domande in modo chiaro, sei già avanti rispetto alla maggior parte degli incidenti.
Perché gli utenti possono caricare una “scatola misteriosa” che la tua app conserva e poi potrebbe mostrare ad altri. Questo può portare a:
Quasi mai è solo “qualcuno ha caricato un virus”.
Conservare significa mantenere dei byte da qualche parte. Servire significa consegnare quei byte ai browser e alle app.
Il rischio nasce quando l’app serve upload utente con lo stesso livello di fiducia e le stesse regole del sito principale. Se un file rischioso viene trattato come una pagina normale, il browser potrebbe eseguirlo (o gli utenti potrebbero fidarsi troppo).
Un default più sicuro è: conserva privatamente, poi servi tramite risposte di download controllate con header sicuri.
Usa deny by default e verifica l’accesso ogni volta che un file viene scaricato o visualizzato.
Regole pratiche:
Non fidarti dell’estensione del file o del Content-Type inviato dal browser. Valida sul server:
Gli outage derivano spesso da abusi semplici: troppi upload, file giganteschi, o connessioni lente che impegnano le risorse. Tratta ogni byte come costo.
Buone pratiche:
Considera ogni richiesta come possibile abuso.
Sì, ma con attenzione. Gli URL firmati permettono al browser di caricare/scaricare direttamente dallo storage senza rendere il bucket pubblico.
Buone impostazioni di default:
Il direct-to-storage riduce il carico sull’API, ma rende vincolanti scoping e scadenza.
Il pattern più sicuro è:
pendingLa scansione aiuta, ma non è una garanzia. Usala come rete di sicurezza, non come unico controllo.
Approccio pratico:
La regola chiave: “non scansionato” non deve mai significare “disponibile”.
Servi i file in modo che il browser non li interpreti come pagine web.
Impostazioni raccomandate:
Content-Disposition: attachment per i documentiContent-Type scelto dal server sicuro (o )/uploads/ va bene”La maggior parte dei bug reali sono banali “vedo il file di un altro utente”.
Se i byte non corrispondono a un formato consentito, rifiuta l’upload.
cleanquarantinedcleanQuesto evita che file “scan failed” o “in elaborazione” vengano condivisi per errore.
application/octet-streamQuesto riduce il rischio che un upload diventi una pagina di phishing o esegua script.