Alojamiento compartido en Apache Server

El problema que tratamos en esta página se refiere a los riesgos de seguridad cuando nuestro sitio está en un servidor de alojamiento compartido (share hosting). Decíamos que la carpeta C:/Windows/Temp/ (o la que fuese según la instalación de PHP) se usaba para almacenar las sesiones. Otros usuarios que comparten el alojamiento podrían llegar a ver sesiones que no eran suyas, por lo que decimos que nuestras sesiones quedan expuestas a ser leídas. Y aunque no lo hicieran ellos directamente, sí podría algún tercero aprovechar huecos de seguridad en sus páginas. Para ver estos efectos, montaré mi servidor local como alojamiento compartido.

El archivo de configuración httpd.conf de mi servidor local Apache 2.2.15 se encuentra en la carpeta conf del servidor. Ahí podemos encontrar las opciones relativas a los suplementos de configuración:


# Supplemental configuration
#
# The configuration files in the conf/extra/ directory can be 
# included to add extra features or to modify the default configuration of 
# the server, or you may simply copy their contents here and change as 
# necessary.
...
# Virtual hosts
Include conf/extra/httpd-vhosts.conf 
...

En este archivo los comentarios comienzan con #, e inicialmente la opción Include estaba puesta como comentario. Quitándole ese caracter pasa a ser una opción que configura el suplemento httpd-vhosts.conf que se encuentra en la carpeta conf/extra/, archivo que viene inicialmente por defecto para ayudarnos a configurar un servidor de alojamiento compartido. Se trata de alojamientos virtuales (vhosts = virtual host) para lograr el alojamiento compartido que necesitamos. Es virtual porque todos los sitios comparten el mismo servidor, aunque externamente esto no afecta al usuario que verá cada sitio como si estuviera alojado en un servidor propio. Esta configuración la vamos a modificar para incluir dos dominios que apunten a una misma IP pero que puedan compartir el servidor como si fueran sitios distintos.

Usando Apache en local, el servidor busca el dominio local por defecto, que suele ser localhost con la IP 127.0.0.1. Esa IP no es accesible desde Internet y se usa para que un ordenador se identifique a si mismo en los protocolos de Internet. En todo caso también puede explicitarse con una opción del archivo httpd.conf que por defecto viene anulada con la marca de comentario #ServerName localhost:80, pues siempre es la misma. Pero podemos cambiar ese nombre de dominio en Windows de tal forma que entonces tendremos que hacérselo saber a Apache en esa opción.

Para cambiar ese dominio o agregar nuevos en Windows, vamos a la carpeta C:/Windows/system32/drivers/etc/ y encontraremos un archivo de texto hosts sin extensión. Lo abrimos con el Notepad o similar y vemos algo como esto (incluso con los comentarios en español):


# Copyright (c) 1993-1999 Microsoft Corp.
#
# Éste es un ejemplo de archivo HOSTS usado por Microsoft TCP/IP para Windows.
#
# Este archivo contiene las asignaciones de las direcciones IP a los nombres de
# host. Cada entrada debe permanecer en una línea individual. La dirección IP
# debe ponerse en la primera columna, seguida del nombre de host correspondiente.
# La dirección IP y el nombre de host deben separarse con al menos un espacio.
# 
#
# También pueden insertarse comentarios (como éste) en líneas individuales
# o a continuación del nombre de equipo indicándolos con el símbolo "#"
#
# Por ejemplo:
#
#      102.54.94.97     rhino.acme.com          # servidor origen
#       38.25.63.10     x.acme.com              # host cliente x
127.0.0.1       localhost    
    

Vemos que viene por defecto con localhost en 127.0.0.1. Ahora agregamos dos nuevos dominios simplemente añadiendo líneas con cualquier nombre como localhost1 y localhost2:

... 
127.0.0.1       localhost
127.0.0.1       localhost1
127.0.0.1       localhost2    
    

Ahora vamos al archivo de suplemento del alojamiento compartido (virtual host) httpd-vhosts.conf y marcamos como comentarios el ejemplo que aparece para que nos sirva de consulta y sólo dejamos como líneas sin comentar las siguientes:

NameVirtualHost *:80

<VirtualHost *:80>
    DocumentRoot ".../Apache server/htdocs/home/sitio"
    ServerName localhost
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot ".../Apache server/htdocs/home/sitio1"
    ServerName localhost1
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot ".../Apache server/htdocs/home/sitio2"
    ServerName localhost2
</VirtualHost>

Los 3 puntos suspensivos previos son un trozo de ruta que he obviado para simplificar la exposición. Realmente mi Apache local lo tengo instalado en la carpeta de usuario C:/Documents and Settings/Administrador/Mis documentos/web, aunque la instalación por defecto seguramente lo hará en C:/Archivos de Programa/ o similar, pero esto no es importante para lo que estamos viendo.

Dentro de htdocs uno puede montar las carpetas como desee. Aquí home será la carpeta que agrupe a todos los dominios compartidos donde habrá una carpeta para cada dominio virtual. Pero también las carpetas de los dominios podrían colgar directamente de htdocs. Esto es sólo una cuestión de nombres sin mayor importancia, pues al final se suben los documentos incluidos dentro de la carpeta sitio al alojamiento real pero no se sube esa carpeta ni las que anteceden, por lo que podría usarse cualquier nombre en el servidor local a efectos del desarrollo del sitio.

Inicialmente ya se habrán creado las carpetas home/sitio dentro de la carpeta htdocs que viene por defecto con la instalación de Apache. En primer lugar hay que poner el dominio virtual que ya existe especificando la ruta raíz que apunta a ".../home/sitio". Luego se deja el nombre de servidor que viene por defecto en Windows: localhost.

A continuación se crean los otros dos dominios virtuales apuntando a .../home/sitio1 y con el nombre localhost1 para el servidor (y lo mismo para sitio2). El primer dominio virtual que se especifique será el que Apache muestre si se le realiza una consulta a un nombre de dominio y no lo encuentra en este archivo httpd-vhosts.conf.

Para ver que el ejemplo funciona hemos de crear la carpeta .../htdocs/home/sitio1 y .../htdocs/home/sitio2 y meter en ella algún documento para que el servidor lo abra. Para acabar rápido copiamos el que viene con la instalación index.html y que está ubicado en la carpeta .../htdocs, añadiéndole algún parráfo.

Vemos en un navegador el dominio localhost:

localhost

Y los nuevos dominios localhost1 con ese párrafo que le hemos añadido:

localhost1

así como localhost2

localhost2

Con esto ya estamos en condiciones de probar localmente un ejemplo de alojamiento compartido. Hay que observar que en una situación real probablemente no podemos acceder a modificar las configuraciones del servidor Apache (los .conf) ni a los de PHP (php.ini). Por lo tanto cualquier solución al problema del riesgo de acceso a la carpeta /Temp pasa por lo que podamos hacer sin tocar esos archivos de configuración.

Además se plantea otro problema relacionado con esas configuraciones. Por motivos de seguridad, el servidor real puede tener desactivadas todas las lecturas de configuraciones e incluso de las versiones que usa tanto de Apache como de PHP. Por ejemplo con la configuración del php.ini:

; Decides whether PHP may expose the fact that it is installed on the server
; (e.g. by adding its signature to the Web server header).  It is no security
; threat in any way, but it makes it possible to determine whether you use PHP
; on your server or not.
; http://php.net/expose-php
expose_php = On

podemos desactivar que se presente la siguiente cabecera, obtenida con mi telnet con php llamando a mi localhost:

   
HTTP/1.1 200 OK
Date: Sun, 26 Sep 2010 11:46:12 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13

simplemente poniendo expose_php = Off, guardando el archivo php.ini y reiniciando el servidor, vemos que ahora con la misma petición sólo sale la referencia de Apache y no la de PHP:

HTTP/1.1 200 OK
Date: Sun, 26 Sep 2010 11:47:02 GMT
Server: Apache/2.2.15 (Win32)

Es obvio que cuanto menos sepa un atacante sobre un sistema, más esfuerzo le requerirá efectuar alguna intromisión no deseada. Por lo tanto hay que partir de la base de que en un alojamiento compartido no contaremos con la ayuda del servidor para reducir el riesgo de nuestro sitio. Y esta forma de actuar nos lleva a no considerar las medidas de seguridad globales del servidor que no podamos gestionar, sino sólo a tener en cuenta aquellas que están a nuestro alcance. Que el servidor tiene medidas de seguridad globales, pues mejor, pero si esas medidas fallan al menos quedarán las de nuestro sitio compartido para intentar hacer frente a la intromisión.

Un explorador de carpetas con PHP

Para calibrar el riesgo de acceso a carpetas, he construido un explorador de carpetas y archivos con PHP. Se trata de una página PHP que no podrá ver en línea aquí por motivos de seguridad como luego entenderá, pero cuyo código puede consultar, copiar e instalarlo en su servidor local si lo desea.

Si lo instala en su servidor local verá que puede "navegar" por todas las carpetas de su ordenador si no hay restricciones. E incluso llegar a la carpeta C:/Windows/Temp y ver el contenido de una sesión cualquiera. Esta imagen ofrece una vista de un acceso a una sesión almacenada de las que usamos en los ejemplos del tema anterior. Se reconstruye la imagen eliminado el resto de entradas del directorio /Temp con unas líneas de puntos rojos para no extender en exceso la imagen y poder ver al final el contenido de la variable de sesión color-fondo:

explorador php

Se trata de un formulario que envía la ruta de una carpeta o la de un archivo. Muestra todo el contenido de esa carpeta y si es un archivo además lo lee y lo presenta. La potencia de PHP para manejar ficheros es evidente. Si no hay restricciones, un usuario de un dominio compartido podría acceder a cualquier carpeta del ordenador donde está el servidor.

En esta aplicación he agregado la lectura dos opciones de configuración de PHP que restringuen el acceso a carpetas. Son safe_mode y open_basedir que analizaremos a continuación. Además se leen los uid y gid, identificadores de usuario y grupo que también analizaremos.

Configuración safe_mode en PHP

La configuración safe_mode se traduce por modo seguro pero tal como expone el manual PHP, se trata de una opción desaconsejada en la versión 5 y probablemente ya no se incluya en posteriores versiones. Según dice el manual, es un intento de resolver el problema de seguridad de los servidores compartidos. Pero añade que es estructuralmente incorrecto intentar resolver este problema a nivel de PHP.

Por lo tanto y siguiendo mi criterio expuesto antes, si estoy en un alojamiento compartido será mejor ignorar este modo seguro y, aunque estuviese activada, montar nuestro sitio como si esta opción no existiera. De todas formas vamos a echarle un vistazo rápido. En el archivo de configuración php.ini en mi servidor local viene así:

; Safe Mode
; http://php.net/safe-mode
safe_mode = Off

Vemos que ya por defecto en la versión 5.2.12 viene desactivada. Se supone que cuando está activado (On), PHP chequeará si el propietario del script coincide con el propietario del fichero con el que se va a operar. El caso es que safe_mode actúa de tal forma que si estoy accediendo con un script del cual soy propietario a otra carpeta de otro propietario, entonces se supone que safe_mode lo impedirá.

En el manual de PHP, en security and safe mode, hay un parte donde explica como funciona, la cual exponemos aquí literalmente y con la traducción:

When safe_mode is on, PHP checks to see if the owner of the current script matches the owner of the file to be operated on by a file function or its directory. For example: (Cuando safe_mode está activado, PHP comprueba si el propietario del script que se está ejecutando coincide con el propietario del fichero donde ese script quiere operar mediante una función que manipula ficheros o directorios. Por ejemplo:)

-rw-rw-r--    1 rasmus   rasmus       33 Jul  1 19:20 script.php 
-rw-r--r--    1 root     root       1116 May 26 18:01 /etc/passwd

Running script.php: (Ejecutando script.php:)

<?php
 readfile('/etc/passwd'); 
?>

results in this error when safe mode is enabled: (aparecerá este error cuando safe mode está activado:)

Warning: SAFE MODE Restriction in effect. The script whose uid is 500 is not 
allowed to access /etc/passwd owned by uid 0 in /docroot/script.php on line 2
(Aviso: Restricción SAFE MODE actuando. El script cuyo uid is 500 no le
está permitido acceder a /etc/passwd cuyo propietario es uid 0 en 
/docroot/script.php, línea 2)

Pero mi servidor local está montado en Windows XP, donde el sistema de grupos y usuarios es diferente que el de este ejemplo que se corresponde con el SO de UNIX y similares. De todas formas intenté montar las carpetas de los dominios compartidos localhost1 y localhost2 de tal forma que sus propietarios fuesen diferentes. Usando la forma en que Windows configura permisos y creando dos usuarios, hice que uno de ellos poseyera las carpetas de home/sitio1 y el otro usuario poseyera home/sitio2. Tuve que sacar estas carpetas de Documents and Settings que pertenece al usuario administrador y montarlas por fuera, de tal forma que pudiera traspasar la posesión. Puse un ejemplar del script explora.php en cada sitio, comprobando que cada uno tenía su propia posesión sobre su script.

Antes de probar el ejemplo puse safe_mode = On en el php.ini y reinicié el servidor. Lamentablemente al ejecutar desde, por ejemplo, localhost1 el script pude alcanzar la carpeta de localhost2 y cualquier otra carpeta del ordenador. E igual resultado para localhost2. En definitiva, en Windows no parece que safe_mode lea adecuadamente quiénes son los propietarios de los archivos y carpetas, pues siempre sale uid=0 y gid=0, por lo que para PHP todos los archivos y carpetas de mi ordenador son del mismo propietario.

De todas formas el único interés era comprobar el funcionamiento, pues dado que safe_mode es una configuración que desaparecerá en versiones futuras de PHP, es mejor no basar la seguridad en ella. Además si estamos en un alojamiento compartido y esta opción estuviera activada, pues mejor, pero en todo caso como dije es mejor no tenerla en cuenta y basar la seguridad de nuestro sitio en configuraciones que podamos controlar. Y safe_mode no la podremos controlar porque en un alojamiento compartido no tendremos acceso al archivo php.ini para cambiarlo.

Las configuraciones del php.ini pueden modificarse en tiempo de ejecución con ini_set() pero no en todos los casos. Por ejemplo, safe_mode tiene la condición PHP_INI_SYSTEM que quiere decir que sólo puede cambiarse en php.ini y httpd.conf. Sin embargo open_basedir que veremos a continuación tiene la condición PHP_INI_ALL lo que significa que puede cambiarse también en ejecución, aunque para versiones anteriores a 5.3 tiene la condición PHP_INI_SYSTEM. Estos dos enlaces del manual PHP lo aclaran:

Configuración open_basedir de PHP

Esta configuración open_basedir también puede limitar el acceso a carpetas. Veámos que pone el archivo php.ini de mi servidor local al respecto:

; open_basedir, if set, limits all file operations to the defined directory
; and below.  This directive makes most sense if used in a per-directory
; or per-virtualhost web server configuration file. This directive is
; *NOT* affected by whether Safe Mode is turned On or Off.
; http://php.net/open-basedir
open_basedir = 

Podemos traducirlo como "si se activa open_basedir, limita todas las operaciones con ficheros al directorio especificado. Esta configuración tiene mayor utilidad cuando se comparten webs en el mismo servidor. Esta configuración no es afectada si safe mode está activado o desactivado". En el manual PHP encontramos la parte sobre PHP instalado como modulo apache (en mi servidor local está instalado así) que explica el uso de esta directiva como una medida más simple que usar una configuración basada en permisos de grupos y usuarios.

Por lo tanto hay tres cosas por las que debemos considerar open_basedir:

  • Es independiente de safe_mode, configuración que no podíamos controlar en el alojamiento compartido real.
  • Con las últimas versiones de PHP (5.3) puede modificarse en tiempo de ejecución. Yo tengo instalado en local el PHP 5.2.13 y, aunque debería actualizarlo, por ahora prefiero terminar esta serie sobre sesiones antes de hacer ningún cambio.
  • Se puede incluir una directiva en el archivo httpd-vhost.conf de la forma php_admin_value open_basedir para cada sitio, pero esto tampoco estará en nuestra mano en un alojamiento compartido real.

Valdrá la pena realizar algunas pruebas en mi servidor compartido local, que antes lo dejamos con esta estructura de carpetas:

...
Apache server
    cgi-bin
    conf
    error
    htdocs
        home
            sitio  (localhost)
            sitio1 (localhost1)
            sitio2 (localhost2)
            ...
    

La primera prueba se basa en limitar cualquier acceso por encima de la carpeta htdocs, configuración que pondremos en php.ini:

open_basedir = ".../Apache server/htdocs"

Ubicamos el script del explorador PHP en los tres sitios, en su carpeta raíz. Ahora vamos a abrirlo en localhost sin especificar open_basedir y verá que se muestra en esta captura de pantalla:

sin open_basedir

En cada exploración tenemos dos primeras carpetas: ./ que señala a sí misma y ../ que nos lleva a la carpeta que contiene a la actual, con lo que podemos navegar "hacia arriba" por el directorio. Ahora activamos open_basedir y le ponemos como carpeta tope .../htdocs. Si intentamos navegar hacia atrás con la carpeta de nivel superior ../, cuando lleguemos a htdocs e intentemos seguir subiendo, veremos que no muestra esa carpeta superior:

con open_basedir

Observe que la segunda carpeta ../ ahora está puesta como un archivo .. sin vínculo. Se ha desactivado con el open_basedir. Además si en el script del explorador PHP no tenemos control de mostrar errores, lo cual podíamos hacer usando el caracter @ en las funciones que manejan carpetas, entonces en la cabecera de esta página saldrá un mensaje como este:

Warning: is_dir(): open_basedir restriction in effect. File(C:\Documents and Settings\Administrador\Mis documentos\web\Apache server\htdocs/..) is not within the allowed path(s): (C:\Documents and Settings\Administrador\Mis documentos\web\Apache server\htdocs\) in C:\Documents and Settings\Administrador\Mis documentos\web\Apache server\htdocs\home\sitio\explora.php on line 87 ......

Este mensaje nos dice que se produce una restricción sobre la función is_dir() usada en la carpeta htdocs cuando intenta mostrar la carpeta superior ../. También salen mensajes sobre otras funciones como is_readable(), pero que hemos obviado por simplicidad.

A continuación vamos a incluir una configuración open_basedir en el archivo de Apache httpd-vhosts.conf para cada uno de los dominios virtuales que están en mi servidor local:

<VirtualHost *:80>
    DocumentRoot ".../Apache server/htdocs/home/sitio"
    ServerName localhost
    php_admin_value open_basedir ".../Apache server/htdocs/home/sitio"     
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot ".../Apache server/htdocs/home/sitio1"
    ServerName localhost1
    php_admin_value open_basedir ".../Apache server/htdocs/home/sitio1"    
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot ".../Apache server/htdocs/home/sitio2"
    ServerName localhost2
    php_admin_value open_basedir ".../Apache server/htdocs/home/sitio2"     
</VirtualHost>

Así cada sitio virtual se limitará a ver las carpetas de su sitio y no otras. Por ejemplo, arrancando el explorador del sitio localhost1 veremos esto:

open_basedir

Aquí se observa que la segunda entrada no es la carpeta ../ sino los dos puntos .. sin vínculo, por lo que no podremos seguir subiendo por el directorio para alcanzar algo que esté fuera del sitio1.

Esto está bien, pero no deja de ser una configuración realizada en el archivo httpd-vhosts.conf del servidor. ¿Qué pasa si nuestro servidor real no tiene esta configuración y otros dominios virtuales que comparten el mismo servidor pueden acceder a la carpeta Temp para ver mis sesiones?. Pero también puede suceder que el servidor esté correctamente configurado para impedirlo, pero que de alguna forma alguién se salte esa protección. Incluso alguién podría utilizar otro lenguaje de programación que no sea PHP y que permita acceso a carpetas, de tal forma que ni safe_mode ni open_basedir se lo pueda impedir. Pero entonces, ¿cómo evitar al menos que alguién vea nuestras sesiones?

Protegiendo las variables de sesión

Cuando necesitemos usar sesiones hemos de pensar que las variables de sesión se almacenarán en una carpeta Temp, recurso al que otros pudieran tener acceso en un alojamiento compartido como hemos visto antes. Si esas variables no contienen datos importantes podríamos dejar las cosas así, pero en otro caso caben la siguientes opciones:

  1. Usar session_set_save_handler() para redefinir la forma en que se almacenan las variables de sesión.
  2. Seguir usando la carpeta Temp que exista para almacenar las sesiones, pero cifrando los datos.

Podríamos haber añadido otra opción: olvidarse del alojamiento compartido y buscar un servidor dedicado. Si tenemos un sitio con procesamiento de datos sensibles es obvio que esta es la única solución que parece más segura. Pero también requiere más conocimiento pues estará a nuestro cargo toda la configuración del servidor. Por ahora mi propósito es usar un alojamiento compartido por dos razones, una económica pues son más baratos y otra por falta de conocimientos suficientes para hacerme cargo de un servidor por completo.

La posibilidad de usar session_set_save_handler() se sale de mis posibilidades actuales. El problema es que hay que "construir" un manejador de sesiones, es decir, el código necesario para guardar las sesiones en algún sitio, en un archivo o en una base de datos. Así este recurso podemos ubicarlo dentro de nuestro sitio y protegerlo, siendo exclusivamente para almacenar nuestras sesiones, de tal forma que el alojamiento compartido no suponga una amenaza tan severa como cuando se albergan en la carpeta Temp.

El manual PHP expone un ejemplo de un manejador usando archivos, donde se presentan las funciones para abrir y cerrar sesiones, escribir y recuperar variables de sesión, etc. Como estoy empezando en estos temas de seguridad, creo que no es conveniente limitarse a copiar y pegar código sin saber como funciona. Y lo del manejo de archivos en PHP o las bases de datos son temas sobre los que quiero adentrarme con más profundidad en otra ocasión. Por lo tanto por ahora sólo contemplaré la protección de los datos almacenados en las variables de sesión. Una forma de hacerlo es cifrando los datos de esas variables, tal como veremos en el siguiente tema.