|
|
|
En este artículo
vamos a explicar el manejo de sesiones en PHP, su utilidad, las funciones y las
directivas de configuración implicadas en el manejo de sesiones.
Primero que nada,
¿qué son las sesiones de PHP? Las sesiones son una facilidad que permite
vincular información a un visitante a lo largo de sus diversos accesos a
nuestro sitio web. Un visitante puede acceder a varias páginas de nuestro
sitio, las sesiones nos ayudan a identificarlo y a vincularle información.
Algunos usos típicos
de las sesiones son:
Mejorar la
experiencia del usuario almacenando información de preferencias como colores o elementos de navegación preferidos.
Almacenar
información de estado (ej: si el usuario está logeado o no).
En el caso de una
aplicación de comercio electrónico llevar registro de los productos que ha
agregado a su carro de compras, o de productos que ha comprado anteriormente
para establecer sus preferencias.
Estos son solo unos
ejemplos pero las posibilidades son infinitas. En general, cuando querramos
almacenar información en relación a un visitante de forma persistente a lo
largo de su visita las sesiones nos facilitarán la vida.
Podríamos
imaginarnos a la sesión como un número estampado en la frente del visitante que
nos ayuda a identificarlo en cada página que visite de nuestro sitio. Además
para cada cada número asignado a un visitante habrá un locker donde guardar sus cosas. Así es que quien está a la entrada
(nuestro código de manejo de sesiones) al llegar un visitante escoge un número
para él y se lo estampa en la frente para que lo utiliice durante su visita.
Para poner a
funcionar una sesión se necesitan cumplir dos requisitos fundamentales:
Nuestro
identificador único es una cadena única aleatoria con la que marcamos al
visitante. Llamaremos a este identificador único session_id. No tendremos que preocuparnos
por generar el session_id pues el sistema de sesión se encarga automáticamente
de generarlo.
Ahora, dado que el
session_id es generado del lado del servidor, hay que encontrar alguna forma de
que esa información persista a través de los diferentes accesos del mismo
visitante. No es posible vincular el session_id con la dirección IP del usuario
ya que hay varios casos en que la misma IP puede ser utilizada por varios
usuarios, por ejemplo cuando varios usuarios están tras un proxy o tras un router
que está haciendo IP masquerade. Por lo tanto para que el session_id esté
disponible junto a cada solicitud de página del usuario es necesario propagar
el session_id para que esté presente junto al visitante en cada acceso y así
podamos identificarlo. Esto se llama propagación de la sesión y más adelante
veremos cómo se hace.
Cada sesión comienza
con la función session_start(), de la forma:
bool session_start(void)
session_start() siempre devuelve
Una vez iniciada la
sesión podremos almacenar información de la sesión en el array $_SESSION ( o $HTTP_SESSION_VARS en versiones anteriores a PHP 4.0.6).
Este es una array asociativo, además es superglobal lo que quiere decir que su
alcance se extiende a todo el ámbito de la aplicación y no tendrás que
declararlo como
Hagamos un ejemplo.
Supongamos que tenemos en línea una página con la foto de George la cacatúa con
el siguiente código:
Ejemplo 1 (george_la_cacatua.php)
<?php
// iniciar la sesión
session_start();
// registrar actividad en la sesión
$_SESSION['actividad'] = "¡He visto a George la cacatúa!";
?>
<h1>stás en la página de George la cacatúa</h1>
<img title="George la cacatúa" src="cacatua.jpg">
<a href="pagina2.php">Visita la siguiente página</a>
session_start() comienza la sesión si no la hay, o continúa con la
sesión del visitante si esta ya existía; la siguiente sentencia se encarga de
guardar en la posición "actividad" del array asociativo de la sesión
la cadena: "¡He visto a George la cacatúa!". Esta información estará
disponible aún cuando el visitante acceda otras páginas de nuestro sitio, y
esta es la magia de las sesiones.
Podemos hacer sobre
el array $_SESSION cualquier operación que podemos hacer sobre una un array.
Esto nos dá una interfaz muy intuitiva y familiar para operar con la
información de la sesión del usuario.
Operación |
Ejemplo |
Crear un elemento |
$_SESSION['elemento'] = valor |
Eliminar un elemento |
unset($_SESSION['elemento']) |
elemento ha sido creado |
isset($_SESSION['elemento']) |
Vaciar la sesión |
$_SESSION = array(); |
Existe como
alternativa al uso directo del array $_SESSION el uso de las funciones session_register(),
session_unregister(),
session_is_registered()
y session_unset()
para operar sobre la información de la sesión. Sin embargo, existen varios
problemas en su uso que pueden hacernos introducir fallos. session_register()
registra variables globales por lo que nos tendremos que cuidar en el alcance
de las variables que registremos. Estas funciones solo
trabajan cuando la directiva register_globals de nuestro php.ini está activada, cosa que no
es nada recomendable por motivos de seguridad, y por lo mismo desde PHP 4.2.0
en adelante el valor por defecto de register_globals es off. Así que no es nada recomendable utilizar estas funciones,
pero si vas por ellas de todas formas o tienes un código que ya las utiliza
abundantemente ten en cuenta que no se deben utilizar el método de acceso
directo al array $_SESSION y las funciones al mismo tiempo, se debe utilizar
uno o el otro.
Sigamos pensando en
nuestro ejemplo. Ahora, al visitar la página de George la cacatúa nuestro
visitante hace click en el enlace a pagina2.php, el código de esta página es
así:
Ejemplo 2 (pagina2.php)
<?php
// iniciar sesión
session_start();
echo $_SESSION['actividad'];
?>
De nuevo session_start()
crea o continúa la sesión, como nuestro visitante ya tiene una sesión la
continúa, cargando sus datos en el array $_SESSION. Entonces por arte de magia
la sentencia con el echo imprimirá:
¡He
visto a George la cacatúa!
¿Cómo ha llegado el
valor de $_SESSION['actividad'] hasta esta página? Ha
llegado por la sesión del visitante. Los valores almacenados en la sesión son
serializados en un archivo al cerrar cada script y al continuar la sesión en
otra página son cargados en el array $_SESSION.
El archivo de la
sesión suele estar en el directorio /tmp en los sistemas UNIX o en c:\tmp en
windows y tiene por nombre el session_id del visitante. Podemos configurar
dónde queremos serializar las sesiones con la directiva session.save_path
de nuestro php.ini o
en tiempo de ejecución mediante la función session_save_path() de esta
forma:
string session_save_path(string);
Si llamamos a session_save_path()
sin parámetros nos devuelve el save_path actual, si le pasamos una cadena la
toma como el nuevo save_path. Si vas a modificar el save_path con esta función
tienes que llamarla antes de llamar a session_start().
Ahora ¿Cómo llega el
llamado a session_start() en pagina2.php a conocer que el visitante
ya tiene una sesión y continuarla? La única forma que tiene de hacerlo es que
le proporcionemos de alguna forma el session_id del visitante, esto es lo que
se logra por la propagación del session_id y vamos a explicar en datalle a
continuación.
Hay dos formas de propagar el session_id:
La propagación del
session_id por URL implica escribir el session_id en el query string de cada
enlace interno de nuestro sitio. Un enlace con un session_id podriá verse de la
siguiente forma:
<a href="pagina2.php?PHPSESSID=F513fad624vDx3">Visita la siguiente página</a>
Donde la variable
PHPSESSID del query string se está encargando de pasar el session_id para la
sesión del visitante a la página pagina2.php. El nombre de la variable que
porta el session_id es igual al nombre de la sesión, y puede definirse con la
directiva session.name de nuestro php.ini
o en tiempo de corrida por la función session_name().
Para hacer más fácil
la escritura del session_id en cada URL está disponible la constante SID
que contiene la cadena "nombre_de_sesion=session_id", que en nuestro
caso sería "PHPSESSID= F513fad624vDx3".
¿Cómo quedaría
nuestro ejemplo de George la cacatúa si propagáramos el session_id por URL?
Aquí está nuestra segunda versión de la página:
Ejemplo 3
(george_la_cacatua_reloaded.php)
<?php
// iniciar la sesión
session_start();
$_SESSION['actividad'] = "He visto a George la cacatúa!";
?>
<h1>Estás en la página de George la cacatúa</h1>
<img title="George la cacatúa" src="cacatua.jpg">
<a href="pagina2.php?<?php echo SID; ?>">Visita la siguiente página</a>
Nota que en el
enlace a pagina2.php hemos embebido un echo de la constante SID,
por lo que nuestro enlace se vería así:
<a href="pagina2.php?PHPSESSID=F513fad624vDx3">Visita la siguiente página</a>
Por suerte y para
hacerlo más fácil PHP puede encargarse de transformar por nosotros todos los
enlaces internos de nuestro sitio para que incluyan el session_id. Esto depende
de dos cosas, de que php esté compilado para hacerlo (con soporte para trans_sid)
y de que la directiva session.use_trans_sid en nuestro php.ini esté activada (valor igual
a "1"). PHP 4.2.0 viene compilado por defecto con esta funcionalidad,
si utilizamos de PHP 4.1.2 para atrás tendremos que
recompilar PHP pasando el parámetro --enable-trans-sid al script configure
antes de la compilación (en UNIX).
Por motivos de
seguridad la directiva session.use_trans_sid viene con el valor
"0" por defecto, de modo que para habilitarla deberemos editar
nuestro php.ini o
utilizar la función ini_set().
ini_set("session.use_trans_sid",
"1");
Si la directiva está
habilitada y PHP ha sido compilado con soporte para trans_id entonces un enlace
escrito de la siguiente forma en nuestra página:
<a href="pagina2.php">Visita la siguiente página</a>
será transformado
automáticamente por PHP en el siguiente enlace:
<a href="pagina2.php?PHPSESSID= F513fad624vDx3">Visita la siguiente página</a>
propagando por
nosotros el session_id en cada URL.
¿Cómo funciona la
propagación por cookie? De estar habilitada la propagación por cookie session_start()
buscará en un cookie el session_id del visitante, si no lo encuentra creará una
nueva sesión, creará un cookie para la sesión y almacenará en el el session_id
del visitante. En un próximo llamado de session_start(),
la función leerá el cookie que contiene el session_id y podrá continuar la sesión
correspondiente.
Como vemos session_start()
se encarga de crear y leer el cookie, así que el proceso es automático y no
tendríamos que preocuparnos por él.
En nuestro Ejemplo 1 y Ejemplo 2, de George la cacatúa y
la pagina2.php, asumimos que el session_id se propaga desde la primer página a
la segunda por cookie. De no haber estado habilitada
la propagación por cookies en pagina2.php no se hubiera continuado la sesión
del visitante pues tampoco estábamos haciendo propagación del session_id por
URL. La sesión hubiera quedado desvinculada del
visitante y el echo $_SESSION['actividad']; no hubiera mostrado nada.
Como en el caso de
la propagación por URL, el nombre del cookie que contiene el session_id es el
mismo que el nombre de la sesión. Por defecto es PHPSESSID pero podemos
modificarlo con la directiva session.name de nuestro php.ini o con la función session_name().
La propagación por
cookie estará habilitada según el valor de la directiva session.use_cookie
en nuestro php.ini.
El valor por defecto es "1" o sea que está activa. Si queremos
desactivar la propagación del session_id por cookie debemos cambiar session.use_cookie
a "0" en nuestro php.ini,
o si no tenemos acceso al php.ini
o queremos cambiarla en tiempo de corrida a nivel de aplicación, podemos
hacerlo mediante la función ini_set() de la siguiente forma:
ini_set("session.use_cookie",
"0");
Hasta ahora hemos
visto como abrir o continuar una sesión con session_start(), cómo
registrar valores en la sesión haciendo asignaciones sobre el array $_SESSION,
y cómo propagar el session_id por cookies o por URL; ahora veremos como cerrar
una sesión.
Para cerrar una
sesión y terminarla completamente tenemos la función session_destroy().
Esta función se encarga de hacer desaparecer la sesión de nuestro servidor,
(elimina el archivo de /tmp o de nuestro session.save_path). Si se solicita una
página y se le envía el session_id de una sesión destruída session_start()
reportará un error pues la sesión no existe. session_destroy() no elimina
las posibles variables globales creadas con la sesión (correspondientes a cada
posición de $_SESSION si teníamos activado register_globals) ni vacía el array
$_SESSION. Por supuesto que esto ocurrirá al cerrar el script, pero si queremos
asegurarnos de que la sesión muera al instante que hacemos el session_destroy() tendremos que encargarnos de vaciar la sesión nosotros
mismos. Así es que una destrucción típica de sesión se vería así:
Ejemplo 4 (destruyendo una sesión)
<?php
// crear o continuar la sesión
session_start();
// vaciarla
$_SESSION = array();
// destruirla
session_destroy();
?>
No es una
comparativa que sea necesaria llevar a cabo. Son herramientas diferentes y
muchas veces complementarias. Pero si conoces el uso de los cookies
posiblemente te hayas planteado que las sesiones te permiten hacer exactamente
lo mismo que los cookies. Pues bien, te mencionamos al menos dos cosas que no
puedes lograr con cookies para que veas la diferencia:
No depender del uso de cookies. Bastante obvio, si utilizas cookies dependes de ellos. ¿Por qué
querría alguien no depender de cookies? Porque por principio de diseño no es
bueno depender de ellos en el caso de funcionalidades medulares de nuestra
aplicación web. ¿Por qué? Por el simple hecho de que muchos usuarios pueden no
aceptar los cookies, o no aceptar todos los cookies
que les enviamos. Si utilizas sesiones puedes no depender del uso de cookies
propagando las sesiones por URL.
Mantener la información fuera del alcance del cliente. Muchas veces la información que quieres almacenar
sobre el visitante no debería ser accedida por él, por ejemplo información
administrativa o información de estado de un usuario (¿está logueado?). No hay
forma de lograr esto con cookies pues los cookies están al alcance del
visitante, se almacenan en el cliente y pueden ser modificados por él. En el
caso de las sesiones no es así pues la información de cada sesión se almacena
en el servidor fuera del alcance del visitante.
En la propagacción
del session_id es donde encontramos el punto en el que más
cuidado tendremos que tener al manejar sesiones. Vamos a discutir dónde
se encuentran las posibles brechas al sistema y algunas medidas defensivas que
podemos tomar. En todos los casos mencionaremos las brechas conocidas, aunque
la posibilidad de que alguien las explote sea tan pequeña que parezca ridículo
mencionarlas. La relevencia de cada una de ellas la juzgaremos según el sistema
que estemos desarrollando.
Como hemos dicho
anteriormente en este método el session_id se pasa de una página a otra de
nuestra aplicación web incluyéndolo como una variable en el query string de
cada URL. Una URL con session_id podría verse así:
http://www.example.com/index.php?PHPSESSID=A86fad765xvc
De modo que con tan
solo conocer uno de los URLs de nuestro sitio que el usuario legítimo está
visitando un atacante podría tomar su papel ante nuestro sistema. El método de
realizar el ataque es muy sencillo, pues solo se tiene que acceder a nuestro
sitio con un URL que incluya la variable PHPSESSID con el valor substraído. Por
ejemplo:
http://www.example.com/account.php?PHPSESSID=A86fad765xvc
Así que la brecha de
este sistema de propagación del session_id se presenta en cualquier situación
en la que el URL pueda llegar a ser descubierto. Planteemos algunos escenarios
en los que esto puede suceder:
1) Enlaces a páginas externas.
Si tenemos enlaces a
páginas externas hemos de cuidarnos de no imprimir el sesion_id en ellos. Si
estamos utilizando la transformación de URLs de PHP para que inserte el
session_id por nosotros no nos enfrentaremos a este riesgo, pues PHP solo
reescribe con el session_id las URLs relativas, dejando intactas las URLs
absolutas que son usualmente las que llevan a otros sitios.El riesgo que se
corre al pasar el session_id a un sitio externo es que un webmaster
malintencionado capte ese session_id y lo utilice. Así que allí nuestro primer
punto a cuidar, no imprimir el session_id en enlaces a sitios externos.
Dentro del mismo
caso hay otra brecha. La mayoría de los navegadores web incluyen la URL del
referer en cada solicitud a un servidor web. El referer es literalmente el
referido, o el sitio que está enviando la visita. La información del referer
suele almacenarse en los logs de los servidores web para hacer análisis de
tráfico. Lo cierto es que si desde nuestro sitio el usuario legítimo hace click
en un enlace externo, aunque nos hallamos cuidado de no imprimir en él es
session_id es muy probable que el sitio remoto reciba la información del
referer, que en este caso es la URL completa de nuestro sitio incluyendo el
query string donde está el session_id. Si el servidor remoto está almacenando
en sus logs los referers tendrá allí el session_id de nuestro usuario el cuál
podrá ser utilizado para impersonarlo. ¿Podemos evitar que se envíe el referer
al sitio remoto? No, de ninguna manera, pues esto es algo que hace el
navegador. El único caso en el que los navegadores no envían el referer es en
el caso que se proceda desde una conexión segura. Es decir que si nuestro sitio
está implementado con HTTP sobre SSL ( o https), el
navegador no enviará la información de referer al sitio externo. Así es que si
tenemos enlaces externos y no queremos que nuestro session_id llegue a ser revelado por el navegador tendremos que implementar
nuestro sitio sobre https. Esto nos protegerá además en el caso de que un
atacante muy dedicado esté a la escucha de nuestro tráfico de red con un
sniffer, leyendo los requests de HTTP a nuestro servidor web.
2) Brechas introducidas por el
usuario.
Puede que hayamos
hecho todo para que el session_id no pase a sitios externos, pero aún así hay situaciones
en las que el URL puede ser tontamente revelada por nuestro propio usuario
legítimo. Desconociendo la relevancia de la información en el URL nuestro
usuario podría pasar un enlace de nuestro sitio a otra persona, vícitima de
ingeniería social, dándole sin saber la información necesaria para que tome su
rol. Además, el URL puede ser agregado en los Favoritos por nuestro usuario o
quedar guardado en el cache del navegador o historial, donde otro usuario de la
misma máquina pueda leerlos y utilizarlos o donde un usuario remoto puede
substraerlos en alguna oportunidad. De modo que por parte del usaurio hay
también gran riesgo de revelar el session_id a un atacante.
En este método el session_id
se escribe en un cookie en el sistema del usuario así que la seguridad se
comprometerá en cualquier caso en el que el contenido del cookie sea revelado.
En la propagación
por cookie existen muy pocos casos en los que el session_id pueda ser revelado.
No hay posibilidad de que el visitante revele el cookie al visitar otro sitio o
al pasar un enlace de nuestro sitio a otra persona. El atacante tendría que ser
muy dedicado y estar escuchando el tráfico de nuestra red para "ver
pasar" el cookie o tendría que atacar directamente a la máquina de nuestro
usuario para substraerle el cookie. De nuevo, podemos protegernos de la
posibilidad de un siffer escuchando el tráfico de red utilizando https y
enviando el cookie de la sesión solo sobre https.
Si optamos por
propagar la sesión por cookie además de habilitar session.use_cookies en
nuestro php.ini o por
ini_set(),
será bueno deshabilitar la propagación del session_id en las URLs, podremos
hacerlo asegurándonos de que esté deshabilitada la directiva session.use_trans_id
de nuestro php.ini y
en el caso de php 4.3.0 en adelante habilitando session.use_only_cookies
para asegurarnos que el session_id solo se propague por cookies.
Como ya hemos
mencionado la información de cada sesión se almacena en un directorio
establecido por la directiva session.save_path, si dicho directorio
fuera legible por otros usuarios del sistema estos podrían leer los session_id
y la información de cada sesión de usuario. Hemos por lo tanto de establecer
restricciones a nivel del sistema de archivos para impedir que usuarios no
autorizados de nuestro propio sistema tengan acceso a las sesiones de usuario.
En todos los casos
dos buenas medidas de defensa que podemos llevar a cabo son, hacer expirar las
sesiones a un tiempo razonable, y conservar información de la sesión que nos
ayude a detectar cualquier anomalía.
Un método muy común
es llevar un registro de sesiones, (en una tabla o archivo) donde incluyamos de
cada sesión de nuestro sistema la hora de última actividad de la sesión y la
dirección IP del usuario que comenzó la sesión. En cada acceso del usuario
haremos dos controles, que no haya pasado cierto tiempo preestablacido desde la
última actividad y que la IP del usuario sea la misma que la IP del usuario que
comenzó la sesión. Si la sesión ha expirado podremos destruirla y solicitarle
que comienze una nueva sesión. Si la IP de usuario que hace el acceso es
distinta a la IP del usuario que comenzó la sesión habrán dos posibilidades,
que el usuario haya rotado la IP como sucede bajo ciertas conecciones, como las
establecidas sobre PPPoE, o que se trate de alguien impersonando a nuestro
usuario legítimo. En todos los casos, no debería suceder que el usuario rote su
IP, o es al menos un caso muy poco probable, por lo que sería bueno hacer este
control y caducar la sesión para impedir un posible ataque.