Mai fidarsi dell'utente! È una buona regola che andrebbe sempre seguita nello sviluppo di un sito Web, soprattutto quando si permette agli utenti di caricare file sul server. Ci sono infatti due insidie che principalmente incombono dietro all'upload dei file e sono:
- Le dimensioni dei file. Non è solo un problema di banda o di limiti di spazio: se non limitate, infatti, le dimensioni dei file trasmessi potrebbero essere tali da compromettere le prestazioni del sito, fino a renderlo temporaneamente indisponibile.
- Il contenuto dei file. È evidente che non si può permettere all'utente di trasmettere qualunque tipo di file, altrimenti potrebbe essere caricato del malware che potrebbe compromettere la sicurezza del nostro sistema.
È indispensabile, quindi, porre dei vincoli all'upload dei file.
Controlli lato cliente e lato server
Lato client, il controllo può essere effettuato via JavaScript. A questo livello, il controllo viene effettuato prima che il file venga inviato al server, evitando, quindi, i lunghi tempi di attesa del controllo server-side (parliamo di file di dimensioni considerevoli), e garantendo, quindi, una buona user experience.
Tuttavia il controllo lato client può essere facilmente aggirato: potrebbe bastare, infatti, anche solo disattivare l'esecuzione degli script.
Lato server, il controllo viene effettuato solo dopo il caricamento del file. Questo secondo livello di controllo offre maggiori garanzie di sicurezza, sebbene renda comunque necessario l'upload dei file.
L'ideale, a questo punto, è duplicare il controllo, eseguendolo una prima volta sul client, in modo da limitare i tentativi di upload ai soli utenti che hanno disattivato l'esecuzione degli script (che dovrebbero essere davvero pochi), e una seconda volta sul server, in modo da garantirsi contro utenti malintenzionati.
Supponiamo, quindi, di avere un semplice form con il solo campo di tipo file:
<form action="" method="post" enctype="multipart/form-data" id="my_form">
<input type="file" name="my_file" id="my_file" />
<input type="submit" name="submit" value="Submit" />
</form>
Vediamo come far sì che l'utente carichi file delle dimensioni e del tipo voluto.
Il controllo via JavaScript
Tutti i maggiori browser supportano la File API di HTML5. Questa permette di accedere a tutte le informazioni associate ai file selezionati dall'utente tramite un campo di tipo file o un'operazione di Drag & Drop. Tali informazioni, come le dimensioni ed il mime-type, permettono di controllare l'input dell'utente e ridurre i fattori di rischio associati all'upload. Consideriamo, quindi, il seguente script:
document.forms[0].addEventListener('submit', function( evt ) {
var file = document.getElementById('my_file').files[0];
var regex = /^(image/)(gif|(x-)?png|p?jpeg)$/i;
if( file && file.size < 1048576 && file.type.search(regex) != -1 ) { // 1MB
alert ('File size: ' + file.size + '; Mime-Type: ' + file.type + ' - Success!')
} else {
alert ('File Not Allowed!')
evt.preventDefault();
}
}, false);
L'API fornisce un riferimento all'oggetto FileList
, dal quale si accede ai singoli file per recuperare le informazioni necessarie.
La soluzione del puro JavaScript, nonostante la semplicità, presenta un inconveniente: qualora il file superi le dimensioni volute (o non sia del tipo richiesto), non è possibile azzerare il solo input
di tipo file
. Si tratta di una garanzia di sicurezza, in quanto, se fosse possibile azzerare l'attributo value
, sarebbe anche possibile passare un valore arbitrario, e questo renderebbe accessibile il computer dell'utente via JS.
Si potrebbe resettare l'intero form, ma questa soluzione presenta degli inconvenienti sul piano dell'usabilità.
Un'alternativa semplice ed elegante prevede il ricorso ai metodi replaceWith
e clone
di jQuery:
jQuery(document).ready(function($) {
$('form input[type=file]').each(function(){
$(this).change(function(evt){
var input = $(this);
var file = input.prop('files')[0];
var regex = /^(image/)(gif|(x-)?png|p?jpeg)$/i;
if( file && file.size < 2 * 1048576 && file.type.search(regex) != -1 ) { // 2 MB (this size is in bytes)
alert( 'Success!' );
}else{
alert( 'File non ammesso - Tipo: ' + file.type + '; dimensioni: ' + file.size );
input.replaceWith( input.val('').clone(true) );
evt.preventDefault();
}
})
});
});
replaceWidth
sostituisce l'elemento su cui è invocato con tutto ciò che viene passato come argomento: in questo caso, è una copia dello stesso elemento input
, cui è stato azzerato l'attributo value
. Con questo codice, provate ora a caricare un file di dimensioni superiori a 2Mb o di un tipo diverso da un'immagine.
Le impostazioni del file php.ini
Prima di affidarsi al controllo finale sul server, si hanno ancora a disposizione un paio di soluzioni client-side. Una prima opzione è quella di ricorrere alle direttive upload_max_filesize
, post_max_size
, max_input_time
e max_execution_time
del file php.ini.
Il problema di questa soluzione è che le direttive si applicano a tutti gli upload, e quindi potrebbe non essere possibile assegnarvi un valore sufficientemente basso. Inoltre, non è detto che si abbia accesso al file php.ini.
Nel caso si operi su server Apache, però, è possibile sovrascrivere le impostazioni generali a livello di directory, tramite il file .htaccess. Il vantaggio che offre questa alternativa è che le nuove impostazioni si applicano solo alla directory dove risiede il file .htaccess
e alle relative sub-directory. Ecco un esempio:
<IfModule mod_php5.c>
php_value upload_max_filesize 2M
php_value post_max_size 8M
php_value max_execution_time 300
php_value max_input_time 300
</IfModule>
Infine, è possibile modificare le impostazioni direttamente dallo script PHP:
ini_set('upload_max_filesize', '5M');
ini_set('post_max_size', '10M');
ini_set('max_execution_time', 300);
ini_set('max_input_time', 300);
Il campo nascosto MAX_FILE_SIZE
La seconda opzione è quella di aggiungere al form un campo nascosto, in modo da fornire a PHP le dimensoni massime del file:
<form action="" method="post" enctype="multipart/form-data">
<!-- MAX_FILE_SIZE must precede the file input field -->
<input type="hidden" name="MAX_FILE_SIZE" value="2097152" />
<input type="file" name="my_file" />
<input type="submit" name="submit" value="Submit" />
</form>
Se le dimensioni del file superano quelle impostate, PHP non permette di caricare il file.
Il campo nascosto MAX_FILE_SIZE
deve necessariamente precedere il campo di tipo file e il suo valore corrisponde alle dimensioni massime ammesse per l'upload (2Mb nell'esempio qui sopra). In questo caso, è il client a fornire l'informazione allo script.
Purtroppo, come le altre soluzioni client-side, anche questa presenta l'inconveniente di essere facilmente aggirabile, rendendo comunque necessario il controllo finale sul server.
Il controllo sul server via PHP: l'array globale $_FILES
Una volta sul server, lo script PHP dovrà verificare le dimensioni del file, rese disponibili dall'array globale $_FILES
. Nella tabella che segue, l'indice my_file
dell'array è dato dal valore dell'attributo name
dell'elemento input
:
Proprietá | Descrizione |
---|---|
$_FILES['my_file']['name'] |
Il nome originale del file sul computer dell'utente |
$_FILES['my_file']['type'] |
Il mime type del file, disponibile se il browser ha fornito l'informazione. Il mime type non è comunque verificato da PHP dal lato server |
$_FILES['my_file']['size'] |
Le dimensioni del file caricato, in byte |
$_FILES['my_file']['tmp_name'] |
Il nome del file come archiviato nella cartella temporanea del server |
$_FILES['my_file']['error'] |
Il codice errore associato all'operazione di upload |
Per impostazione predefinita, i file caricati vengono collocati provvisoriamente nella cartella temporanea del server, a meno che non vengano memorizzate impostazioni diverse nel file php.ini.
Quello che segue è lo script utilizzato nei nostri test:
<?php
if( ( !empty( $_FILES["my_file"] ) ) && ( $_FILES['my_file']['error'] == 0 ) ) {
if( preg_match( '/^(image/)(gif|(x-)?png|p?jpeg)$/i', $_FILES['my_file']['type'] ) && ($_FILES["my_file"]["size"] < 2097152) ){
$path = 'tmp/' . basename( $_FILES['my_file']['name'] );
if( move_uploaded_file($_FILES['my_file']['tmp_name'], $path) ){
print_r( $_FILES['my_file'] ); /* File salvato correttamente */
}else{
print "Impossibile salvare il file: " . $_FILES['my_file']['error'];
}
}else{
echo "errore nel tipo (" . $_FILES['my_file']['type'] . ") o nelle dimensioni (" . $_FILES["my_file"]["size"] . ")";
}
}else{
print_r( $_FILES['my_file'] );
}
Lo script verifica che l'array $_FILES
non sia vuoto e che il codice di errore corrisponda a 0 (file caricato correttamente). In caso negativo, verrà visualizzata la struttura dell'array e i valori dei singoli elementi. Se nel form è presente il campo nascosto MAX_FILE_SIZE
, il codice errore sarà 2.
Nel caso, invece, nel form non fosse presente il campo nascosto, la prima condizione sarà verificata e il controllo sarà effettuato dalla seconda condizione, in cui saranno testati il mime-type e le dimensioni del file, memorizzate in $_FILES["my_file"]["size"]
.
$_FILES['my_file']['tmp_name']
è il nome del file come archiviato nella cartella temporanea del server. Questo file rimane nella directory fino a quando lo script che ha trasmesso i dati rimane in esecuzione. Per mantenere copia dei file, lo script deve provvedere a spostarli in un'altra directory.
Segue l'array che rappresenta un file caricato con successo:
Array
(
[name] => thumbnail.png
[type] => image/png
[tmp_name] => /tmp/phpB98zzX
[error] => 0
[size] => 4388
)
Il controllo sulle estensioni
Normalmente, le impostazioni predefinite del server non permettono l'esecuzione dei file immagine, ma queste impostazioni possono essere aggirate aggiungendo una doppia estensione al file.
Per gli utenti dei sistemi Unix/Linux, è possibile eliminare il rischio a livello di directory, creando un file .htaccess
che limiti l'accesso ai soli file con l'estensione appropriata:
deny from all
<Files ~ "^w+.(gif|jpe?g|png)$">
order deny,allow
allow from all
</Files>
I permessi su un file .php
Sempre nei sistemi *nix, una ulteriore accortezza può essere quella di modificare i permessi sui file, in modo che sia inibita l'esecuzione dopo il caricamento (chmod 0666 o 0604). Ciò in quanto l'intestazione di un un file immagine potrebbe contenere del codice PHP maligno: il server riconosce lo script e lo esegue. A questo scopo si può anche ricorrere alle funzioni PHP chmod()
e umask()
.
Link utili
Per approfondire gli argomenti trattati in questo articolo, segnaliamo le seguenti letture: