Subir un fichero de forma segura
19/02/2009
Lo que parece algo trivial como permitir a los usuarios de nuestra aplicación en php subir un fichero a través de un formulario web, puede ocasionarnos graves problemas de seguridad en nuestro servidor.
Debemos, por tanto, establecer controles de seguridad en nuestro script de upload en php para no permitir que nos lleguen ficheros como el siguiente.
<?php
system($_GET['comando']);
?>
Éste es un script sencillo con el que podemos ejecutar comandos de shell a través de la url http://servidor/shell.php?comando=comando_shell_unix. Pero existen muchos más avanzados y peligrosos.
En el siguiente script de upload en php, tenemos en cuenta las siguientes verificaciones para evitarlo.
- Verificar el "content-type"
- Verificar la extensión del fichero
- Acceso indirecto a los ficheros subidos
<?php
$imageinfo = getimagesize($_FILES['userfile']['tmp_name']);
if($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg') {
echo "Sólo se aceptan ficheros GIF y JPEG \n"; # Es un ejemplo, podemos montar un array con los tipos de fichero permitidos
exit;
}
$blacklist = array(".php", ".phtml", ".php3", ".php4", ".php5");
foreach ($blacklist as $item) {
if(preg_match("/$item\$/i", $_FILES['userfile']['name'])) {
echo "No está permitido subir ficheros PHP\n";
exit;
}
}
$uploaddir = '/var/spool/uploads/'; # Fuera del web root
$uploadfile = $uploaddir . basename($_FILES['userfile']['name']);
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
echo "Fichero válido y fue correctamente subido.\n";
} else {
echo "Error al subir el fichero.\n";
}
?>
Ahora no es posible acceder a /uploads/ para ver los ficheros subidos, por lo tanto es necesario el siguiente script adicional para obtener los ficheros.
<?php
$uploaddir = '/var/spool/uploads/';
$name = $_GET['name'];
readfile($uploaddir.$name);
?>
Todavía podemos darle una vuelta más de seguridad generando un nombre aleatorio a cada archivo subido y almacenar en un base de datos el valor de referencia.
<?php
require_once 'DB.php'; # Métodos de acceso a la base de datos
$uploaddir = '/var/spool/uploads/'; # Fuera del web root
$uploadfile = tempnam($uploaddir, "upload_");
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
# Guardar información sobre el fichero en la BD
$db = DB::connect("datos_conexion");
$res = $db->query("INSERT INTO uploads SET name=?, original_name=?, mime_type=?", array(basename($uploadfile, basename($_FILES['userfile']['name']), $_FILES['userfile']['type']));
}
?>
En éste caso, el script para acceder al fichero subido podría ser el siguiente.
<?php
require_once 'DB.php'; # Métodos de acceso a la base de datos
$uploaddir = '/var/spool/uploads/';
$id = $_GET['id'];
$db = DB::connect("datos_conexion");
$file = $db->getRow('SELECT name, mime_type FROM uploads WHERE id=?', array($id));
if(is_null($file) || count($file)==0) {
die("Fichero no encontradro");
}
header("Content-Type: " . $file['mime_type']);
readfile($uploaddir.$file['name']);
?>