Manejo de sesiones en PHP

Introducción

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.

¿Qué son las 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.

¿Cómo funcionan las sesiones en PHP?

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:

  1. Asignar al visitante un identificador único (escoger un número).
  2. Propagar este identificador único a través de los diferentes accesos del visitante para que podamos reconocerlo (estampárselo en la frente).

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.

Creando una sesión

Cada sesión comienza con la función session_start(), de la forma:

bool session_start(void)

session_start() siempre devuelve true. En caso de que el visitante no tenga una sesión la crea y en el caso de que ya la tenga la continúa. Si ya tenía una sesión tiene que haber una forma de que podamos vincularla al usuario. Para ello el session_id tiene que haber sido pasado a la página actual, y esto se hace por cookie, por GET o por POST. Ten en cuenta que si la propagación es por cookie session_start() se encargará de crear un cookie, entonces es necesario llamarla antes de enviar cualquier cabecera HTTP, esto es antes de comenzar a imprimir nuestra página pues sino habrá un error al tratar de crear el cookie.

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 global al utilizarlo dentro de funciones.

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.

Propagación del session_id

Hay dos formas de propagar el session_id:

  1. Propagación por URL
  2. Propagación por cookie

Propagación por URL

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>

Propagación automática del session_id en cada URL

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.

Propagación por cookie

¿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");

Cerrando una sesión

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();
 
?>

¿Sesiones Vs. cookies?

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.

Consideraciones de seguridad en el manejo de sesiones

En la propagación del session_id

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.

Brechas de seguridad en la propagación del session_id por URL

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.

Brechas de seguridad en la propagación del session_id por cookies

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.

Permisos de sistema de archivos en nuestro session.save_path

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.

Algunas medidas de defensa

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.