<< Indietro
Il file system secondo PHP19/11/2006 10:19
Php
Come realizzare un completo sistema di upload/download file sicuro ed affidabile, con relative mini-statistiche di scaricamento.

Introduzione
Vi sarete sicuramente imbattuti tante volte nel a necessità di far caricare dei files sul server dai vostri utenti, tramite il meccanismo di upload e soprattutto tramite il web, quindi direttamente dal vostro sito e con un sistema completamente automatico.
Molti di voi saranno riusciti in questo scopo, altri forse no, ma avete comunque mai pensato ai problemi di sicurezza a cui si va incontro con una procedura del genere?
Oltre al 'upload magari vorreste anche dare la possibilità di scaricare questi files, e qui altri problemi di sicurezza si pongono, forse anche più gravi o gravosi dei precedenti. Cercherò di dare una risposta a tutte queste domande,creando insieme a voi un completo sistema di upload/download dei files, con tanto di sistema per avere del e mini statistiche discaricamento.

Quali sono i problemi di sicurezza?
Vi starete chiedendo a quali problemi di sicurezza si va incontro quando si compiono operazioni come l'upload o il download di file
via web.
Ebbene nel caso del 'upload uno dei maggiori problemi a cui si va incontro è che potremo ricevere sul nostro server dei file indesiderati, come virus, file troppo grossi o altre amenità, ad esempio una persona esperta potrebbe tranquil amente inviare uno
script in perl o php o altro linguaggio e potrebbe riuscire a richiamarlo dal web senza troppe difficoltà, il problema potrebbe nascondersi anche nel nome del file inviato, un utente smaliziato potrebbe facilmente provocare dei seri danni a tutto il sistema.
Nel caso di download invece il maggior problema è sicuramente l'esposizione a tutti del a locazione ove si trova il file da scaricare,directory che sicuramente ha dei permessi particolari per poter permettere la scrittura dentro e quindi, in teoria, esposta ad ogni
sorta di danno. A tutti questi problemi, e ad eventuali altri, porremo rimedio insieme, nel a costruzione del a nostra miniapplicazione.

Creiamo il necessario per fare l'upload sul server
Prima di partire con la creazione del nostro script di upload, dobbiamo conoscere esattamente la versione di PHP che stiamo usando, perché la gestione degli upload viene affrontata in maniera differente nel e varie versioni del noto parser.
In particolare mi riferirò sempre ad una gestione che prevede disabilitata l'opzione register_globals (scelta comunque consigliata). Nel caso in cui stiamo trattando con versioni precedenti alla 4.1.0 (esclusa, e comunque non considero il PHP3 che funziona in maniera ancora una volta differente) al ora faremo sicuramente riferimento ad una variabile particolare chiamata $HTTP_POST_FILES che dovremo avere l'accortezza di dichiarare global se la utilizzeremo al 'interno di funzioni e/o metodi.
Se invece usiamo una versione del PHP superiore o uguale al a 4.1.0 allora faremo riferimento, con gli stessi e identici meccanismi, al a variabile superglobals $_FILES che non ha bisogno di essere dichiarata global .
Una volta accertato questo, dobbiamo assicurarci che il nostro server sia in grado di accettare degli upload via web e cioè che l'opzione file_uploads nel php.ini sia settata ad on (potete controlarlo direttamente nel php.ini o se non vi avete accesso tramite il comodo phpinfo() ),
Oltre a questo dobbiamo anche conoscere qual è la dimensione massima dei files caricabili, informazione che ci viene data dal 'opzione upload_max_filesize (sempre nel php.ini), di solito tale dimensione è posta a 2 MegaByte, più che sufficiente per la maggioranza dei casi (ma non sempre).
Nel caso in cui l'opzione file_uploads sia assente o settata su off, o che vi venga inviato un file che superi le dimensioni massime impostate, vi ritroverete con l'array $_FILES praticamente vuoto a parte una variabile d'errore settata.Ora abbiamo tutte le informazioni necessarie per poter iniziare a "codare" il nostro piccolo script di upload, dobbiamo prima di tutto creare la pagina html che contiene il form che fa l'upload (riporto solo il form):

 <form action="ul.php" enctype="multipart/form-data" method="post" />
<input type="hidden" name="MAX_FILE_SIZE" value="1048576" />
Documento da caricare: <input type="file" name="documento" />
<br />
<input type="submit" value="Carica" />
</form>

Come potete vedere il modulo è abbastanza semplice, notiamo sicuramente due cose, la prima è l'indicazione del tipo di codifica (enctype = encoding type), quel a impostata ci da la possibilità di inviare files, mentre l'altra è il campo hidden MAX_FILE_SIZE che indica la dimensione massima dei file caricabili, c'è da dire che quest'ultimo control o viene effettuato dal webserver ma è comunque meglio operare lo stesso controlo al 'interno del nostro ul.php, lo script che si occuperà di elaborare i dati inviati dal 'utente.
Prima di vedere come funziona ul.php, dobbiamo assicurarci che sul nostro server vi sia una directory che abbia i permessi di scrittura abilitati (di solito basta un Chmod 666 o 777 se state su Linux, comunque qualsiasi buon programma di FTP vi permette di fare quest'operazione facilmente), mettiamo che creiamo la nostra dir di upload in "/htdocs/doc/ul", quindi "ul" sarà la dir che conterrà i file inviati al server, tenendo conto che i nostri script saranno tutti localizzati in /htdocs.
Un'altra cosa di cui dobbiamo accertarci è il percorso esatto (path assoluto o fisico direbbero alcuni) del a nostra dir al 'interno del server, per far questo utilizzeremo una variabile d'ambiente predefinita del php (fate attenzione perchè come al solito tale variabile cambia a seconda del a versione del
parser con lo stesso gioco che fa la variabile $_FILES), in questo caso avremo a che fare con $HTTP_SERVER_VARS oppure
$_SERVER (la prima nel caso di php<4.1.0 la seconda per php>=4.1.0), ed in particolare faremo riferimento alla chiave
"DOCUMENT_ROOT" che appunto indica il percorso fisico del nostro script.
Certamente mettere i files che si "uploadano" sul server in una dir sicuramente browsabile (ovvero al a quale si può accedere via web, quindi sotto il percorso /htdocs o /var/www/html o altri), non è il massimo del a sicurezza, ma è certamente più semplice da gestire ed è a volte l'unica strada che si può percorrere avendo a disposizione un servizio di hosting.
Infatti la via più sicura è quel la di mettere questi files in una dir al di fuori del ramo browsabile, ma questo significa aver accesso ad altre "location" oltre quel a web,sul server col quale si lavora, ed in genere se avete uno spazio in hosting questo non lo potete fare, mentre potete farlo se avete un vostro server, oppure state lavorando in locale, per esempio per una intranet.
Passiamo ora a vedere lo script vero e proprio, percorrendo le varie fasi che sono necessarie per ottenere una buona sicurezza ed affidabilità. Prima di tutto dobbiamo fare dei control i sul file inviato che possiamo riassumere in questo modo:
·
Che sia stato realmente caricato un file
·
Che le sue dimensioni siano inferiori a quel e massime volute
·
Che il tipo del file sia tra quel i da noi voluti
Sono piccole e semplici operazioni che ci permetteranno di avere un buon control o sul 'upload e ci daranno come risultato uno script
robusto ed affidabile:

$max_size=1048576; // dimensione massima voluta;
  $mime_alowed="png**jpg**pjpeg**jpeg**pdf**doc**word**excel**xls"; // tipi di documenti permessi separati da **
// Controlo prima se un file è stato caricato
if (!is_uploaded_file($_FILES[`documento'][`tmp_name']))
{
echo "Errore: nessun file caricato!";
@unlink($_FILES[`documento'][`tmp_name']);
exit;
}
else
{
// Il file è stato caricato, procediamo con gli altri controlli
}

Come potete vedere con queste semplici istruzioni abbiamo già iniziato ad ottenere uno script sicuro.
Con la funzione is_uploaded_file (disponibile fin dal a versione 3 del php) controlliamo prima di tutto se realmente un file è stato caricato, quindi riusciamo a verificare se è stato inviato realmente un file tramite il protocolo http con metodo POST oppure è stata tentata un'operazione di violazione che un eventuale pirata informatico potrebbe compiere.
Il prossimo passo è quel o di fare dei control i sia sul e dimensioni che sul tipo di file inviato, ma prima voglio spiegarvi un po' com'è costruito l'array $_FILES che stiamo utilizzando:

  $_FILES[`file'][`name']    // Il nome originale del file che si trova sul PC del 'utente
$_FILES[`file'][`type'] // Il mime del file, ovvero il tipo di file che si sta inviando, sempre che il browser fornisca questa informazione
$_FILES[`file'][`size'] // La dimensione in byte del file
$_FILES[`file'][`tmp_name'] // Il nome temporaneo del file compreso il path assoluto di dove si trova sul server
$_FILES[`file'][`error'] //
L'eventuale codice di errore generato ­ disponibile solo da PHP 4.2.0 in poi nel momento in cui lavoriamo col file appena caricato, dobbiamo riferirci alla chiave "tmp_name" che contiene il percorso esatto del file caricato ed il suo nome temporaneo, di solito in sistemi Linux i file vengono caricati nel a dir /tmp e da li poi vanno spostati dove serve e rinominati nel modo più utile. Ora control iamo prima la dimensione e poi il mime type:

 if ($_FILES[`documento'][`size']>$max_size)
{
// Controlo fal ito quindi mando errore a video e cancel o il file
echo "Errore: il file caricato eccede la dimensione massima di $max_size bytes";

 @unlink($_FILES[`documento'][`tmp_name']);
exit;
}
else
{
// Controlo il mime type
$temp=explode(`/',$_FILES[`documento'][`type']);
$mime_uploaded=end($temp);
if ( !stristr($mime_alowed, $mime_uploaded) )
{
// Controlo fal ito, mando errore e cancelo file
echo "Errore: il file caricato è di un tipo non permesso!";
@unlink($_FILES[`documento'][`tmp_name'];
exit;
}
else
{
// Procedo col resto del e operazioni
}

}
Bene ora al a fine di questi pochi statements abbiamo finalmente a disposizione il nostro agognato file, cosa ce ne facciamo?
Semplice, dobbiamo spostarlo nel a nostra dir di upload in modo che sia possibile usufruirne in qualche modo, pochi e semplici sono i passi:

$path=$_SERVER[`DOCUMENT_ROOT'];
$dir='/doc/ul/';
$destination=$path.$dir.$_FILES[`documento'][`name']; // così ripristiniamo il nome originale del file
move_uploaded_file($_FILES[`documento'][`tmp_name'],$destination);
echo `Il file `.$_FILES[`documento'][`name'].' è stato caricato con successo sul server!';
Come vedete qui ho fatto uso di "DOCUMENT_ROOT" che ci dice esattamente dove si trova lo script che ora è in esecuzione, ho poi aggiunto le varie sottodir necessarie, ed aggiunto al fine, il nome del file. Ricordatevi che la dir "ul" deve avere i permessi di scrittura quindi un bel chmod 666 o chmod 777 fatto via FTP dovrebbe risolvere. Questa è una del e principali cause d'errore nella costruzione di script del genere.
Piccoli e ulteriori consigli sul e procedure di upload
La funzione is_uploaded_file non è disponibile su ogni versione del parser, la troviamo sicuramente dopo la 3.0.16 e nel ramo PHP4 dopo la 4.0.2, nel caso in cui non avete una di queste versioni allora potreste costruirvi la vostra personale funzione is_uploaded_file,così come consiglia anche il manuale del php, che vi riporto per comodità di consultazione:

function is_uploaded_file($filename)
{
if( !$tmp_file = get_cfg_var(`upload_tmp_dir') )
{
$tmp_file = dirname(tempnam(`',' ));
}
$tmp_file .= `/'.basename($filename);
return (ereg_replace(`/+','/', $tmp_file) == $filename );
}
questa è una versione abbastanza complessa, ma un control o più semplicistico ma sicuramente non meno efficace può essere effettuato in questo modo:

if ($_FILES[`documento'][`tmp_name']=="none" || $_FILES[`documento'][`tmp_name']=="")
{
// FILE NON CARICATO
}
nel caso abbiate una versione del php che sia maggiore o uguale al a 4.2.0, potete contare su un nuovo elemento, che per comodità non ho utilizzato nel o script precedente, ovvero un messaggio di errore impostato nel a chiave `error' in caso di fallimento

sull 'upload, eccone le specifiche:

$_FILES[`files'][`error']=
0: Nessun errore, file caricato con successo
1: Il file supera in dimensioni le specifiche settate tramite upload_max_filesize
2: Il file supera in dimensioni le specifiche date tramite html e il campo MAX_FILE_SIZE
3: Il file è stato caricato solo parzialmente
4: Nessun file è stato caricato quindi al posto della funzione "is_uploaded_file" potreste anche avere uno statement del genere:

if($_FILES[`files'][`error']!=0)
{
// Qualche errore, manda messaggio in base al 'error code
}
else
{
// Tutto ok , procede
}
ricordatevi però che potete usare questa funzionalità solo se avete un PHP engine del ramo 4.2.0 o superiore.
Ed ora si scarica
Fin'ora abbiamo trattato del caricamento di files sul server, ora vogliamo creare un piccolo script che ci faccia vedere la lista di tutti i files caricati e ci permetta di scaricarli, sempre stando attenti al a sicurezza!!
Dal momento che non abbiamo memorizzato la lista dei files da nessuna parte, come facciamo a fare un elenco di tutti i documenti creati??
Semplice, usiamo le comode funzioni "Directory" messe a disposizione dal 'engine, in particolare facciamo uso del a classe DIR. Iniziamo subito col leggere la dir degli UL e stipiamo tutto in un array:

$ul_dir = $_SERVER[`DOCUMENT_ROOT'].'/doc/ul';
$ul_file = array() ;
$d = dir($ul_dir) ; // creo un nuovo oggetto che leggerà la dir
while ($file=$d->read())
{
if($file !='.' && $file !='..')
$ul_file[]=$file ;
}
$d->close(); // invoco il distruttore del 'oggetto.

Ora nel 'array $ul_file abbiamo l'elenco completo di tutti i file, il codice è abbastanza semplice e si spiega da se, a voi trovare altri
metodi per leggere la dir, ma credo che questo sia il più veloce, sicuro e semplice da implementare.
Presentiamo in una semplice pagina html il contenuto del precedente array, con il relativo link per scaricare:

$ul_dir = $_SERVER[`DOCUMENT_ROOT'].'/doc/ul';
if (is_array($ul_file) && sizeof($ul_file)>0 )
{
foreach($ul_file as $f)
{
if($f!=' && !empty($f) && file_exists($ul_dir.'/'.$f) )
echo `<a href="dl.php?f='.$f.'" title="Clicca per scaricare">'.$f.'</a>';
}
}

Come potete vedere il codice è molto semplice, vi ricordo che è molto importante controllare comunque che il file esista, prima di stampare a video il link per scaricare.
Notate anche che non ho messo un link diretto al file, infatti questo è un altro errore comune,cioè dare direttamente il link del file da scaricare, dicendo praticamente a tutti il path del file, e siccome quel path è "aperto" perché
usato per gli upload, al ora è anche a rischio di attacco. Tenendo conto di ciò, dobbiamo creare un piccolo script che ci permette di
effettuare il download del file ed anche (visto che ci troviamo) di conteggiarne i click.

Per il conteggio dei click ci sono varie strade, potremmo ad esempio creare un unico file di log, in cui scrivere di volta in volta alcuni dati interessanti, come ad esempio la data e l'ora di scaricamento, il nome del file ed infine (per le solite ragioni di sicurezza)
l'indirizzo IP del 'utente che ha effettuato il download. In questo caso però ci sarebbe difficile avere a colpo d'occhio le statistiche per
il singolo file, ora senza scomodare il database (magari usate un db di appoggio come utile esercizio), possiamo semplicemente (anche se questa soluzione non è il massimo in termini di prestazioni) creare un file di statistiche per ogni documento scaricato.

Cosa ci scriviamo dentro? Tre le possibili soluzioni:
·Un numero sequenziale accodandolo al a fine del file
·Leggiamo il file, incrementiamo il numeretto, tronchiamo il file e riscriviamo il numeretto
·Informazioni di log per ogni riga (data,ora,ip)
Direi che la soluzione più "performante" è la prima, ma è quella meno comoda, per la terza si ripropone il problema iniziale, avremo una riga per ogni download, quindi non ci rimane che la seconda soluzione che è leggermente (impercettibilmente direi) meno veloce del a prima ma sicuramente la più leggibile e usabile.

Andiamo a codare:

$ul_dir = $_SERVER[`DOCUMENT_ROOT'].'/doc/ul';
if( isset($_GET[`f']) && ($_GET[`f']!=' ) ) // Control o che sia passato il parametro in get nel a url
{
if ( file_exists($ul_dir.'/'.$_GET[`f']) )
{
// Se il file esiste, al ora creo il log ma prima esplodo il nome del file in modo
// da lavorare meglio
$temp = explode(`.',$_GET[`f']);
$file_name = $temp[0];
$file_ext = $temp[1];
if( file_exists($ul_dir.'/'.$file_name.'.log') )
{
// Il file di log esiste quindi lo apro, leggo il numeretto e lo aggiorno di +1
$log = fopen ($url_dir.'/'.$file_name.'.log','r');
$num_dl = fgets($log);
$num_dl++;
fclose($log);
}
else
{
// Il file di log non esiste, quindi inizializzo $num_dl a 1
$num_dl = 1;
}
// ora apro il file in modalità `w' in modo che venga troncato a zero
$log = fopen ($url_dir.'/'.$file_name.'.log','w');
fputs($log, $num_dl); // Scrivo il numero nel file
fclose($log); // Chiudo il file
// Qui ci vanno le altre operazioni
// **SCARICAMENTO FILE**
}
}

La prima parte è pronta, abbiamo creato il log con il nome del file da scaricare ed estensione ".log", abbiamo aggiornato il numeretto statistico che ci da la misura dei download effettuati e poi abbiamo scritto questo dato nel file log che è stato preventivamente azzerato.

Ora dobbiamo occuparci del a parte più importante ovvero, del 'invio del file al 'utente tramite il browser, l'operazione è abbastanza semplice, ma richiede un po' di attenzione.
Prima di tutto dobbiamo decidere se forzare il download del file o permettere l'apertura del file in base ai plugin installati nel browser (ad esempio se il file da scaricare è un PDF al ora si aprirà acrobat reader ®, per leggerlo), io vi farò vedere come forzare il download del file, a voi l'utile esercizio di far aprire il file dal browser col corretto plugin.

Una cosa da sapere è che per fare quest'operazione, bisogna inviare al browser degli "Header" speciali, ovvero dei comandi particolari che interpretati dal browser vi faranno scaricare il file, nel caso vogliate invece farlo aprire, dovete dire al browser il corretto mime-type ovvero il tipo di codifica del file, control ando l'estensione del file ovviamente.

Una lista esauriente ed abbastanza affidabile dei mime-type del e varie applicazioni la trovate a questa url:

http://oregonstate.edu/aw/faq/mimetypes/.
Procediamo subito col codice, vi consiglio di leggere con attenzione i commenti per capire meglio come funziona il processo:

// Questo codice va dove si trova il commento **SCARICAMENTO FILE**
// cancelo eventuale cache per leggere la dimensione del file
clearstatcache();
// leggo la dimensione
$file_size = filesize($url_dir.'/'.$_GET[`f']);

// Controllo l'utente che browser utilizza
// in base al browser cambia l'header da inviare
// per forzare il download
if( eregi("(msie) ([0-9]{1,2}.[0-9]{1,3})", $_SERVER[`HTTP_USER_AGENT']) )
{
// Internet Explorer
Header("Content-type: application/force-download");
}
else
{
// Netscape o altro
Header("Content-type: application/octet-stream");
}
// Ora mando la dimensione del file
Header("Content-Length: ".$file_size);
// Ora mando il nome del file
Header(`Content-Disposition: attachment; filename="'.$_GET[`f'].'"');
// Ultima operazione, inviare il file
// basta usare una funzione particolare, che legge
// il file e manda il contenuto in output.
@readfile($url_dir.'/'.$_GET[`f']);
Conclusioni
Spero che le indicazioni che vi ho dato, hanno chiarito qualche dubbio, e vi aiutino nel 'approntare qualche nuovo servizio sul vostro
sito. Riassumendo un po' ciò di cui abbiamo parlato, vi ricordo che la sequenza giusta del e operazioni è la seguente:
·
Upload del file tramite il form in una pagina html e lo script ul.php
·
Col egamento al o script che fa la lista dei files
·
Scarichiamo un file tramite lo script dl.php
Al a prossima.

BOX AUTORE
Tommaso D'Argenio si occupa di sviluppo software con tecnologie web da oltre 4 anni, è il coordinatore nazionale della mailing list sul PHP più popolare in Italia (circa 900 membri) attiva sul portale www.ziobudda.net, inoltre è fra i soci fondatori dell 'IRLug (Irpinia Linux User Group) www.irlug.org. Attualmente è impegnato full time nella realizzazione di software server side per importanti realtà aziendali italiane oltre ad essere webmaster dei servizi offerti da www.webstatis.com e www.newmedianet.it.
e-mail: info@holosoft.it oppure rajasi@ziobudda.net
Commenti
Autore Testo
<< Indietro