Seguridad del sistema de archivos
PHP está sujeto a la seguridad misma de la mayoría de sistemas de
servidores en lo que a permisos sobre archivos y directorios se refiere.
Esto le permite controlar cuáles archivos en el sistema de archivos
pueden ser leídos. Debe tenerse cuidado con aquellos archivos que tengan
permisos de lectura globales, para asegurarse de que su contenido es
seguro y no represente peligro el que pueda ser leído por todos los
usuarios con acceso al sistema de archivos.
Ya que PHP fue diseñado para permitir acceso al nivel de usuarios al
sistema de archivos, es completamente posible escribir un script PHP que
le permita leer archivos del sistema como /etc/passwd, modificar sus
conexiones tipo ethernet, enviar trabajos de impresión masivos, etc.
Esto tiene algunas implicaciones obvias, en el sentido en que usted
tiene que asegurarse de que los archivos desde lo que lee y hacia los
que escribe datos, sean los correctos.
Considere el siguiente script, en donde un usuario indica que quisiera
eliminar un archivo ubicado en su directorio personal. Este caso asume
que se trata de una situación en donde se usa normalmente una interfaz
web que se vale de PHP para la gestión de archivos, así que el usuario
de Apache tiene permitido eliminar archivos en los directorios
personales de los usuarios.
Example #1 Un chequeo pobre de variables nos lleva a...
<?php
// eliminar un archivo del directorio personal del usuario
$nombre_usuario = $_POST['nombre_enviado_por_el_usuario'];
$archivo_usuario = $_POST['archivo_enviado_por_el_usuario'];
$directorio_home = "/home/$nombre_usuario";
unlink("$directorio_home/$archivo_usuario");
echo "¡El archivo ha sido eliminado!";
?>
Ya que el nombre de usuario y el nombre del archivo son enviados desde
un formulario de usuario, cualquiera puede enviar un nombre de usuario y
archivo propiedad de otra persona, y eliminarlo aun si se supone que no
tenga ese privilegio. En este caso, usted querrá usar otro método de
autenticación. Considere lo que sucede si las variables enviadas son
"../etc/" y "passwd". El código entonces se ejecutaría efectivamente
como:
Example #2 ... un ataque al sistema de archivos
<?php
// elimina un archivo de cualquier parte del disco duro al que el
// usuario de PHP tiene acceso. Si PHP tiene acceso de root:
$nombre_usuario = $_POST['nombre_enviado_por_el_usuario']; // "../etc"
$archivo_usuario = $_POST['archivo_enviado_por_el_usuario']; // "passwd"
$directorio_home = "/home/$nombre_usuario"; // "/home/../etc"
unlink("$directiorio_home/$archivo_usuario"); // "/home/../etc/passwd"
echo "¡El archivo ha sido eliminado!";
?>
Hay dos importantes medidas que usted debe tomar para prevenir estas
situaciones.
-
Otorgarle únicamente permisos limitados al usuario web del binario
PHP.
-
Chequear todas las variables que son enviadas por usuarios.
Aquí hay una versión mejorada del script:
Example #3 Un chequeo de nombres de archivos más seguro
<?php
// elimina un archivo de cualquier parte del disco duro al que el
// usuario de PHP tiene acceso.
$nombre_usuario = $_SERVER['REMOTE_USER']; // uso de un mecanismo de autenticación
$archivo_usuario = basename($_POST['archivo_enviado_por_el_usuario']);
$directorio_home = "/home/$nombre_usuario";
$ruta_archivo = "$directorio_home/$archivo_usuario";
if (file_exists($ruta_archivo) && unlink($ruta_archivo)) {
$cadena_registro = "Se eliminó $ruta_archivo\n";
} else {
$cadena_registro = "No se pudo eliminar $ruta_archivo\n";
}
$da = fopen("/home/logging/eliminacion_archivos.log", "a");
fwrite($da, $cadena_registro);
fclose($da);
echo htmlentities($cadena_registro, ENT_QUOTES);
?>
Sin embargo, incluso este caso no está libre de problemas. Si su sistema
de autenticación le ha permitido a los usuarios la creación de sus
propios nombres en el sistema, y un usuario elige "../etc/", el sistema
se encuenrta nuevamente expuesto. Por esta razón, puede que uster
prefiera escribir un chequeo más personalizado:
Example #4 Chequeo de nombres de archivos aun más seguro
<?php
$nombre_usuario = $_SERVER['REMOTE_USER']; // uso de un mecanismo de autenticación
$archivo_usuario = $_POST['archivo_enviado_por_el_usuario'];
$directorio_home = "/home/$nombre_usuario";
$ruta_archivo = "$directorio_home/$archivo_usuario";
if (!ctype_alnum($nombre_usuario) || !preg_match('/^(?:[a-z0-9_-]|\.(?!\.))+$/iD', $archivo_usuario)) {
die("Nombre de usuario o archivo incorrecto");
}
//etc...
?>
Dependiendo de su sistema operativo, existe una amplia variedad de
archivos sobre los que usted debería estar atento, incluyendo las
entradas de dispositivos (/dev/ o COM1), archivos de configuración
(archivos /etc/ y los archivos .ini), areas conocidas de almacenamiento
de datos (/home/, Mis Documentos), etc. Por esta razón, usualmente es
más sencillo crear una política en donde se prohíba toda transacción
excepto por aquellas que usted permita explícitamente.
Problemas relacionados con bytes nulos
Dado que PHP usa las funciones de C de bajo nivel para las operaciones
relacionadas con el sistema de archivos, puede que los bytes nulos sean
gestionados de una forma inesperada. Debido a que los bytes nulos
denotan el final de una cadena en C, las cadenas que los contienen no
serán consideradas en su totalidad, sino únicamente hasta que ocurre un
byte nulo. El siguiente ejemplo muestra un segmento de código
vulnerable que demuestra este problema:
Example #5 Script vulnerable a bytes nulos
<?php
$archivo = $_GET['archivo']; // "../../etc/passwd\0"
if (file_exists('/home/wwwrun/'.$archivo.'.php')) {
// file_exists devolverá true ya que el archivo /home/wwwrun/../../etc/passwd existe
include '/home/wwwrun/'.$archivo.'.php';
// el archivo /etc/passwd será incluido
}
?>
Por lo tanto, cualquier cadena que sea usada en una operación del
sistema de archivos debe ser validada apropiadamente siempre. He aquí
una versión mejor del ejemplo anterior:
Example #6 Validación correcta de la entrada
<?php
$archivo = $_GET['archivo'];
// Lista de valores correctos posibles
switch ($archivo) {
case 'principal':
case 'foo':
case 'bar':
include '/home/wwwrun/include/'.$archivo.'.php';
break;
default:
include '/home/wwwrun/include/principal.php';
}
?>