Contacto | #php_para_torpes | Enlázanos | ¿Quiénes somos?
10 usuarios Online (0)
Darse de alta en la web | Recuperar password   
Inicio / Articulos / Guía de seguridad en PHP (Capítulo primero)

Guía de seguridad en PHP (Capítulo primero)

por Aeoris 23 oct 05 32767 lecturas

Autor

Aeoris

Descripción

Una pequeña guía con una serie de ejemplos y consejos para aumentar la seguridad y ser conscientes de los riesgos que hay al publicar nuestra página.

Otros articulos

Índice

Introducción

Objetivo del documento

El objetivo de este documento es que el programador que lo lea adquiera unas nociones básicas (y no tan básicas) sobre seguridad que pueda aplicar a la hora de hacer sus aplicaciones. La mayoría de temas que aquí se presentan son temas reales, que se producen al no tener el programador tales nociones. La mayoría de temas en los que se va a basar el documento están orientados al uso en páginas web.

Entiendo como usuario malintencionado aquel que busca en nuestra aplicación fallas de seguridad y las explota por puro aburrimiento, sin ánimo de reportar esos fallos al programador para que puedan ser subsanados (hay gente que lo hace).

La mejor forma de protegerse es no confiar nunca en cualquier valor que no sea fijo. Si no estás seguro de que una variable va a tener el valor que esperas, o que una llamada a unlink() borra el archivo que quieres, asegúrate, pues puede ser por ahí por donde empiece un ataque.

Asegurarse no consiste en ejecutar el script y decir uy, esto funciona como espero, sino que hay que agotar todas las posibilidades de ataque antes de subir tu página al servidor definitivo.

Cómo leerlo

El documento presupone ciertos conocimientos de PHP, y que el lector sea capaz de poner atención al leer, por lo que se pide encarecidamente que, en caso de que la tengas puesta, quites la música y prestes total atención a lo que hay escrito.

Los bloques de código son ejemplos: los habrá que funcionen y que no. Hay ejemplos de lo que hay que hacer, y también de lo que no hay que hacer, todo con el objetivo de que el lector aprenda a arreglar sus errores viendo código de otra persona.

A lo largo de todo el documento se podrán ver ejemplos que presentan fallos de seguridad, debido a una mala configuración de PHP o un mal uso de las llamadas a las variables.

En otros casos verás ejemplos cuyo código tiene en cuenta los principales aspectos de seguridad. Es muy importante que NO copies y pegues directamente estos códigos en tu página. Antes debes comprender todo lo que se explique para poder aplicarlo.

Directivas de configuración

Archivos de configuración

php.ini
Archivo de configuración de PHP. Puedes ver que php.ini estás usando con phpinfo (más adelante se explica qué es y como usar phpinfo).
httpd.conf
En él guarda su configuración el servidor HTTP Apache; al ser este el más utilizado por desarrolladores y servicios de alojamiento (tanto gratuitos como de pago) es en el que se centrarán los comentarios de este documento. Normalmente se encuentra en el directorio conf en el directorio raíz de Apache.
.htaccess
Fichero especial que usa Apache en el que se pueden poner ciertas llamadas a sus directivas o las de algunos módulos que haya cargado. Requiere permisos para poder utilizarlo. Más adelante se enseña a comprobar si es posible su uso.

Tipos de directivas

PHP_INI_SYSTEM
Las directivas de este tipo se pueden modificar en el php.ini y en el httpd.conf
PHP_INI_PERDIR
Las directivas de este tipo pueden cambiarse en los archivos php.ini, httpd.conf y .htaccess. Para saber como cambiarlas en archivos .htaccess consulta la siguiente sección.
PHP_INI_USER
Según el manual de PHP (en su versión española): La entrada puede definirse en scripts de usuario. Si miramos el manual en inglés (más recomendable, la verdad) veremos que en realidad quiere decir que las directivas pueden ser modificadas en el registro de Windows. Aunque listada en el manual, parece no haber ninguna directiva que sea de este tipo (supongo que se podrán modificar las de tipo PHP_INI_SYSTEM). Al no ser la mayoría de servicios de alojamiento usuarios de Microsoft Windows no explicaré como cambiar estas directivas.
PHP_INI_ALL
Estas directivas pueden ser cambiadas en cualquier parte, incluso en el propio script por medio de ini_set().

Cómo cambiar directivas del tipo PHP_INI_PERDIR

Lo primero que deberías hacer es obtener información sobre la actual configuración del servidor, mediante el uso de estas líneas.

<?php phpinfo() ?>

La función phpinfo() nos muestra información sobre la configuración de PHP y sobre la máquina en la que está instalado. En la página que genera veremos como está ajustado PHP. Es importante familiarizarse con esta función y con su salida, puesto que es básica y se usará muy a menudo.

Antes de explicar como cambiar este tipo de directivas debes asegurarte de que tu servidor cumple una serie de requisitos:

PHP como módulo de Apache
Es fácil saber si PHP está compilado o instalado como módulo de Apache. Simplemente tienes que mirar en el phpinfo() la línea en la que pone Server API tenga un valor como Apache 2.0 Handler.
AllowOverride con el valor Options o All
Esto es un poco más pesado de comprobar, hay que hacer lo siguiente: pon en un archivo llamado .htaccess en el directorio que tengas la página una cadena como asdasd123. Si Apache muestra un Internal Server Error al actualizar esa página es que tenemos permiso para usarlos.

Si cumples estos dos requisitos ya puedes cambiar el valor de register_globals (u otras opciones de configuración de PHP) en el archivo .htaccess del directorio sobre el que queremos aplicar estas reglas (puede que tengas que crearlo):

#Para directivas con valores booleanos (On/Off, 1/0…) se usa php_flag
php_flag register_globals Off

#Para otras directivas se usa php_value
php_value error_reporting 4095

register_globals

No nos engañemos, tener register_globals a on no es inseguro, lo que provoca los fallos de seguridad es el no programar bien y el no tener en cuenta a posibles atacantes. Cualquier variable que no definamos, o cualquiera de la que no comprobemos datos puede ser víctima de envenenamiento de variables, aquí voy a exponer un poco que hacer en estos casos.

La comunidad PHP decidió desactivarlo por defecto (antes estaba activado) a partir de la versión 4.2.0.

Esta directiva inicialmente era de tipo PHP_INI_ALL (se podía cambiar con ini_set() en el php.ini o mediante archivos .htaccess), pero a partir de PHP 4.2.3 es de tipo PHP_INI_PERDIR (se puede cambiar solamente desde el php.ini o los archivos de configuración de Apache). Más abajo se explica como cambiar valores de configuración con este tipo de archivos.

La razón por la que en algunos de los ejemplos de este documento se usará register_globals a on es para que se vean los peligros que corres si lo tienes activado, y como (en medida de lo posible) subsanarlos.

Razones para tenerlo desactivado

Tenerlo activado no hace más insegura una aplicación, ¿pero tenerlo desactivado la hace mas segura? Sí, así es. No porque nos vaya a proteger contra los "malvados juakers que comen donuts y pizza", no es milagroso, pero nos va a forzar a no usar variables globales para referirnos a valores pasados por POST, GET, valores de sesión o cookies.

Matrices superglobales ($_GET, $_POST, $_SESSION…)

Mucho se ha hablado de las variables (o matrices) superglobales (las que debes usar para referirte a lo mencionado anteriormente), pero todavía queda gente que, o bien no las conoce, o bien cree que no son necesarias, o que simplemente no las usa por pura pereza, por no modificar su código (aunque esto le vaya a beneficiar, y mucho). Qué le vamos a hacer, así somos…

A partir de la versión 4.1.0 de PHP están disponibles estas variables, y a partir de YA (si todavía no lo haces) debes usarlas.

La diferencia entre las variables normales (presuponiendo el valor de register_globals activado, que es como lo tienen la mayoría de servicios de alojamiento, gracias a aplicaciones como PHP-Nuke y similares) y las matrices superglobales es muy simple. Usando matrices superglobales no se puede producir envenenamiento de variables normales a través de URL, de cookies falseadas, etc.

La razón de este comportamiento es que register_globals (cuando está activado, obviamente) inyecta los valores de las variables que se pasan al script (lo que en esencia sería como usar la función extract() en cada una de las matrices superglobales), esto es terriblemente dañino si no se sopesan las consecuencias. Imaginemos por un momento que nuestro panel de control comprueba que el usuario está acreditado para acceder a él de la siguiente forma (el ejemplo es de PHP.net):

<?php
if (usuario_valido()) {
    
$autorizado = true;
}

if (
$autorizado) {
    echo
"Bienvenido a mis documentos importantes\n";
} else {
    echo
"No tienes acceso a esta sección\n";
}
?>

Poquitas líneas, ¿eh? Pues contienen varios fallos… Si te fijas es muy simple resolverlos. Vamos a analizar el código:

if (usuario_valido()) {
    
$autorizado = true;
}

Si el usuario es válido definimos la variable $autorizado como true, ¿y si no lo está? ¿no la definimos? Imagina por un momento que acceden a ese documento con la url panel.php?autorizado=1, ¿que pasaría? saldría el bonito mensaje de Bienvenido a mis documentos importantes. Recuerda definir siempre las variables que vayas a usar.

El ejemplo anterior, bien programado sería algo parecido a esto:

<?php
if (usuario_valido()) {
    
//Podemos guardar información útil
    
$_SESSION['autorizado'] = 'Nombre del usuario';
} else {
    
$_SESSION['autorizado'] = false;
}

//$_SESSION['autorizado'] va a estar siempre definida
if ($_SESSION['autorizado'] !== false) {
    echo
"Bienvenido a mis documentos importantes {$_SESION['autorizado']}\n";
} else {
    echo
"No tienes acceso a esta sección\n";
  
//Finalizamos la ejecución de la aplicación, no nos interesa que se siga interpretando código
    
exit;
}
?>
Qué hacer cuando se tiene activado

Puedes intentar desactivarlo usando el método que se explica más arriba. De todas maneras, si programas bien una aplicación (como se detalla en la siguiente sección) es improbable que tener esta directiva activada te afecte en demasía, así que no te preocupes.

Buenas costumbres

A continuación tienes una lista de cosas que debes tener en cuenta:

  • Definir siempre las variables antes de usarlas.
  • Usar matrices superglobales en tus aplicaciones, en detrimento de las antiguas globales.
  • No usar nombres típicos para las variables, ya que aumentas la posibilidad de que un atacante descubra como modificarlas.

error_reporting

Esta directiva, aunque extraordinariamente útil, puede llevar a nuestro atacante a conocer datos sobre nuestras aplicaciones, por lo que distinguiremos dos entornos: el entorno de desarrollo y el entorno de usuario final. Se explicará en que consiste cada una en su correspondiente sección.

error_reporting (tanto la directiva como la función) admiten como parámetro un número entero. Puedes usar las constantes numéricas que provee PHP para el control de errores y combinarlas con operadores bit a bit en el archivo php.ini y en tus aplicaciones, pero no en los archivos httpd.conf o .htaccess.

Los valores por defecto son los siguientes:

PHP 3
El valor por defecto es 7, lo que equivale a E_ERROR | E_WARNING | E_PARSE, pero dado que en PHP 3 no se soportaban las constantes en el archivo php3.ini el valor había de ser numérico.
PHP 4 y PHP 5
E_ALL & ~E_NOTICE. Se muestran todos los errores salvo los de tipo E_NOTICE y E_STRICT (este último solo es aplicable a PHP 5).

Las constantes predefinidas dedicadas al manejo de errores que usaremos habitualmente son estas:

E_ERROR
Errores fatales al ejecutar una aplicación, interrumpen la ejecución del mismo. Estos errores se producen por ejemplo al intentar utilizar funciones no definidas.
E_WARNING
Advertencias en tiempo de ejecución, no detienen la ejecución de una aplicación. Se produce cuando (por ejemplo) se proporciona a una función (como fwrite()) un recurso no válido como parámetro.
E_NOTICE
El tipo menos conocido, junto con E_STRICT, y de los más útiles. Estos errores se producen mayormente al encontrar el intérprete de PHP una variable o constante no definida. No detienen la ejecución.
E_ALL
Agrupa todos los errores excepto los de tipo E_STRICT.
E_STRICT
Está disponible únicamente a partir de PHP 5. Si habilitas este tipo de error PHP lanzará avisos si encuentra código obsoleto para que el programador (o sea, tú) puedas corregirlo y así mantener la compatibilidad e interoperabilidad.

Cada una de estas constantes tiene un valor numérico, puedes ver el valor de cada una imprimiendo la constante en cuestión.

Como este tema tiene bastante miga se va a explicar en una sección aparte.

safe_mode

Es de tipo PHP_INI_SYSTEM y está desactivada por defecto.

Esta directiva es, según el manual de PHP, un intento para resolver el problema de la seguridad en un servicio de alojamiento compartido (el más común, poca gente puede permitirse un servidor dedicado).

Básicamente lo que hace esta directiva al estar activada es comprobar que los ficheros sobre los que operamos desde otro fichero (la aplicación) tienen el mismo dueño (en sistemas UNIX).

Veamos un ejemplo de esto (que puede sonar perfectamente a chino, no te preocupes):

$ ls -l
-rw-r--r--  1 root   root    36 2005-04-09 03:02 fichero1.php
-rw-r--r--  1 aeoris   aeoris    73 2005-04-09 03:02 fichero2.php

Como se puede ver el archivo con nombre fichero1.php pertenece al superusuario (o root) y el fichero2.php a aeoris, ahora veamos que pasa si intento leer el contenido de fichero1.php desde el archivo que me pertenece (fichero2.php) teniendo safe_mode activado.

Warning: file_get_contents(): SAFE MODE Restriction in effect. The script whose uid is 1000 is not allowed to access fichero1.php owned by uid 0 in /var/www/fichero2.php

Al no tener los mismos dueños se produciría un error.

Esto en un momento dado nos podría proteger contra otros usuarios del mismo alojamiento que, por ejemplo, quieran obtener datos sobre nuestras aplicaciones. En el momento en que intentasen hacerlo no podrían.

Pero… no podrían con PHP… con cualquier lenguaje es una cosa trivial hacer un explorador de archivos del sistema sencillito, con lo que puede ver donde están nuestros archivos, y en la mayoría de casos (al no estar en PHP y no tener safe_mode) visualizar el código fuente.

Mi consejo con este tema es que no confíes demasiado en esta directiva, pues no te protege en absoluto. Suele ser más molesta que útil (es mi opinión, no ha de tomarse como un referente).

Teniéndola activada también se restringirán algunas funciones, hay una lista de estas funciones en el manual de PHP que los mismos escritores definen como posiblemente incorrecta e incompleta.

open_basedir

Es de tipo PHP_INI_SYSTEM y tiene valor nulo por defecto.

Lo que hace es limitar el espacio de trabajo al directorio al que está ajustada la directiva. Si quisieramos incluir un fichero por encima de ese directorio lanzaría un error.

Veamos un ejemplo (teniendo en cuenta que open_basedir vale /var/www):

<?php
readfile
('/etc/resolv.conf');
?>

Produciría este error:

Warning: readfile(): open_basedir restriction in effect. File(/etc/resolv.conf) is not within the allowed path(s): (/var/www/) in /var/www/open_basedir.php on line 3

Este tipo de error normalmente se ve al usar la función copy() para mover archivos que se han subido con un formulario. Este comportamiento es erróneo y debería usarse move_uploaded_file() en su lugar.

Esta directiva puede ser útil en un momento dado, pero por lo general dará quebraderos de cabeza innecesarios.

Nomenclatura de archivos

Este es un tema un poco retorcido: ¿a qué me refiero con nomenclatura de archivos? Simplemente al nombre que se les pone a los archivos o directorios (que no son más que archivos que a su vez contienen otros archivos).

Un hacker intentará encontrar el nombre del archivo que contiene datos tales como la contraseña de la base de datos o la contraseña de acceso al panel de control, datos con los que pueda causar un daño, o simplemente probar la seguridad de una aplicación.

Intenta no poner fácil que se averiguen los nombres de los archivos. Con esto quiero decir que no le pongas al panel de administración como nombre admin.php o cpanel.php (si, esto también es aplicable si los tienes en directorios a parte…).

He visto gente que le pone extensión .inc (por include supongo) a sus aplicaciones programadas en PHP. Esto es terriblemente peligroso. Accediendo a la URL de el archivo en el que tengas la conexión a la base de datos por ejemplo tendrían los datos para conectar, con lo que podrían borrarla en un momento dado.

He visto casos de páginas muy importantes y que la mayoría usamos mucho que nombraban a sus archivos con extensiones de este tipo, con lo que obtener el código y hacer un volcado de la base de datos (nombres de usuario, contraseñas, números de tarjeta de crédito…) no fue nada difícil.

Estos dos últimos párrafos vienen a decir que no debes poner bajo ningún concepto extensiones que no interprete PHP por defecto a tus aplicaciones.

Si eres un rato cabezón y no quieres cambiar la extensión (¿demasiado trabajo quizá?) te recomiendo que vetes el acceso a estos archivos de forma directa, puedes poner el siguiente código en un archivo .htaccess (aunque depende de que tu servidor sea Apache, de que puedas usar .htaccess…):

<FilesMatch "\.inc$">
    Order allow,deny
    Deny from all
</FilesMatch>

Las mismas reglas que se explican arriba sobre no poner nombres típicos a los archivos se aplica a los directorios.

Debes cuidar de tener una estructura de directorios limpia y clara de tu aplicación, para trabajar más cómodamente con ella. Esto no significa que debas descuidar los nombres de los directorios; puedes poner un número aleatorio delante del nombre de cada uno para así mantenerlas más o menos fuera del alcance de ciertos elementos.

Errores

Ya se ha hablado un poco de como los errores son mostrados (o no) por PHP; se ha dicho que, cuando la aplicación se encontraba en el servidor público, la mejor opción era ocultar al usuario los errores lanzados por el intérprete y ofrecerle unos más amigables y porque no, carecientes de información relativa al servidor. Como se puede suponer, esto es bueno, pero carece de utilidad real si no sabemos siquiera que esos errores han existido. Debemos ser informados si ocurren, y esto obviamente no lo podemos dejar en manos del usuario.

Mostrar errores según entorno

Dependiendo de donde tengamos alojada la página (en un servidor local o en uno final) deberemos mostrar o no los errores. Antes de que subas la página debes limar la mayoría de asperezas que pueda tener, aquí verás como tener bien configurado el reporte de errores.

Entornos de desarrollo

En estos casos nos interesa que se muestren absolutamente todos los errores, puesto que nos ayudarán a corregir posibles fallas de seguridad (como que no definimos una variable por ejemplo) entre otras cosas.

PHP, como hemos visto antes, ajusta a un valor por defecto la directiva error_reporting, tenemos dos opciones para cambiarla (supongamos que estás en un servidor local, por tanto tienes acceso a todos los ficheros de configuración):

Cambiar la configuración en el php.ini
Esta es la solución más fácil, y también la mejor. En el archivo php.ini localiza la linea del error_reporting y cambia su valor por E_ALL | E_STRICT (si usas PHP 5) o E_ALL si usas PHP 4.
Usar la función error_reporting()
Esta solución es bastante pesada, por lo que no la recomiendo. Se trata de llamar a la función error_reporting() en cada uno de los archivos en los que queramos ajustar el reporte de errores. Debes llamarla con los parámetros con los que se ajustaría la directiva error_reporting (mira el punto anterior).
Entornos de usuario final

Ten siempre en cuenta al usuario, ¿qué leches le importa que no puedas conectar a la base de datos? ¿o que haya un error de tipo sintáctico? Absolutamente nada. También debes considerar que un usuario malicioso podría intentar forzar a que hubiesen errores, consiguiendo de esta manera información que le fuese útil a la hora de intentar vulnerar la seguridad de nuestra aplicación.

¿Qué se puede hacer para remediarlo? Simple. Con las mismas que antes (en el punto anterior, en nuestro servidor local) hacíamos que se mostrasen todos los errores aquí nos interesa que no se muestre ninguno.

Para ello haremos uso de otra directiva de PHP, diferente de error_reporting, se trata de display_errors().

Esta directiva es del tipo PHP_INI_ALL, por lo que puedes cambiarla usando ini_set(). Si eliges cambiarla de este tipo debes llevar cuidado, ya que si se produce un error fatal se mostará (ya que el ini_set() nunca llegará a ejecutarse, puesto que se ha terminado la ejecución antes de empezar a interpretarlo). A estas alturas ya conoces como cambiar los valores de las directivas de otras formas.

Pero… y si se produce un error, ¿cómo nos enteraríamos? ¡Sigue leyendo!.

Registro de errores

Está claro que no todos los errores tienen el mismo impacto en la página, uno por ejemplo puede hacer que salga mal una letra, y otro que una sección no se muestre. Es por ello que debemos ser informados de todos los errores que ocurran en nuestra página, desde el más insignificante hasta los más graves.

PHP dispone de varias maneras de registrar los errores, la más común es usar el registro del sistema en sistemas UNIX (syslog) ajustando esto desde el propio php.ini, aunque no es recomendable, pues no tendremos acceso a este fichero, ya que está restringido al superusuario. Pasamos a explicar una forma correcta, aunque restrictiva (requiere Apache y poder usar ficheros .htaccess) de cazar hasta los errores de interpretación (aunque estos, en teoría, jamás deberían estar presentes en el servidor).

Tenemos que decirle a PHP tres cosas:

  • Que no muestre los errores.
  • Que los registre.
  • Un sitio donde registrarlos.

Ahora bien, ya se ha dicho que si lo hacemos en la misma aplicación y se produce un error de los que paran la ejecución esto no nos serviría de nada, por eso debemos añadir al .htaccess lo siguiente:

php_flag display_errors Off
php_flag log_errors On
php_value error_log "Ruta al registro"

En la tercera instrucción has de poner la ruta completa, a ser posible fuera del directorio visible por el usuario, de un archivo en el cual poner los errores.

Tenemos otro método, que puede usarse para casos puntuales: mandar correos electrónicos. Esto debe ser usado para avisarnos de que la base de datos no conecta, por ejemplo, o que una consulta SQL ha fallado. Nos serviremos de la función mysql_error() para mostrar este caso concreto:

<?php
mysql_connect
(/* datos de conexión varios*/) || (
    
mail('mail@example.org''Fallo al conectar a la base de datos',
    
"MySQL ha devuelto lo siguiente:\n" mysql_error())
    && die(
"<p>Estamos experimentando problemas, vuelva luego</p>\n"));
?>

php-hispano.net 2002 - 2009 | XHTML 1.0
Datos Legales | Webmaster