Sesiones

Iniciado por alexander1712, Enero 30, 2013, 10:05:48 PM

Tema anterior - Siguiente tema

0 Miembros y 1 Visitante están viendo este tema.

Propagar sesiones

El identificador de la relación servidor-cliente en una sesión

En los temas anteriores se hizo una exposición de las sesiones realizando ejemplos con sesiones basadas en cookies. Parecen ser la forma más segura pero también hay otras formas de propagar el identificador de sesión, que recordemos que es un vínculo que relaciona el servidor con un cliente determinado. Este identificador debe alojarse en el navegador del cliente con la finalidad de que en sucesivas peticiones sea remitido al servidor. Así éste podrá seleccionar las variables de sesión individuales para ese único cliente.

En el navegador del cliente podemos alojar el identificador de sesión de al menos tres maneras:

    En una cookie que envía el servidor y que el cliente devolverá con cada petición.
    El servidor lo aplica en todos los elementos que tengan un vínculo. Así el cliente cuando active unos de esos vínculos, el identificador llegará al servidor como un parámetro URL.
    Como campos ocultos de formularios que se envían por POST. Este es similar al anterior, aunque con importantes diferencias en cuanto a seguridad.

En el primer tema, en el apartado sobre el proceso de una sesión PHP, expusimos las siguientes configuraciones de PHP relacionadas con la forma en que se propagan las sesiones.

    session.use_cookies: Con valor activado PHP usará cookies para la propagación, pero si éstas estuvieran desactivadas en el navegador entonces usará URL a menos que la siguiente opción esté activada. Si está desactivado no usará cookies en ningún caso.
    session.use_only_cookies: Si ésta y la anterior están activadas, sólo se usarán cookies. Si ambas están desactivadas sólo se usará URL.
    session.use_trans_sid: Para el caso de URL, si ésta opción está activada, el servidor construirá parámetros URL en todos los elementos que contengan vínculos en la página. El parámetro será el SID, una cadena que concatena el nombre de la sesión y el identificador interponiendo el signo igual nombre_sesión=identificador_sesión, siguiendo los estándares de paso de parámetros por la URL. Por eso la denominación usar SID transparente (use_trans_sid), pues se adjunta el SID en los vínculos de forma transparente para el programador. Los elementos afectados por el SID pueden configurarse en la opción url_rewriter.tags, cuyo valor por defecto es a=href,area=href,frame=src,input=src,form=fakeentry,fieldset=. Se trata de una cadena de parejas elemento=atributo, de tal forma que es en ese atributo donde se incorporará el identificador. Si por ejemplo en nuestro HTML tenemos un elemento como <a href="otra-pagina.html">... entonces PHP agregará <a href="otra-pagina.html?SID" >..., . Para el elemento <form> si se incluye la palabra clave fakeentry entonces PHP incluirá una nuevo campo de texto oculto con el identificador. Si no queremos esto hemos de cambiarlo por form=action para que lo agregue a la URL. Si desactivamos session.use_trans_sid tendremos que agregar manualmente los SID en el código a cada vínculo.

CitarPHP dispone de la constante SID que se actualiza después de hacer un session_start() con el mismo resultado que la cadena session_name()."=".session_id(). Pero en este documento cuando hablemos de SID en general nos estaremos refiriendo a la pareja nombre e identificador de sesión, que son los dos valores a propagar para conseguir la identificación, mientras que escribiremos constante SID en los casos en que deseemos referirnos a esa propia constante de PHP.

En base a como se combinan estas opciones, que son configurables en tiempo de ejecución con ini_set(), podemos establecer la forma de propagación. En esta tabla resumimos todos los ejemplos que desarrollaremos en esta página, donde 1 indica activado y 0 desactivado:




Propagaciónsession.use_cookiessession.use_only_cookiessession.use_trans_sidurl_rewriter.tagsNotas
Sólo cookies 1 1 0 indiferente(indiferente) Opción por defecto en php.ini 5.2.13
Sólo URL con trans_sid y form=fakeentry 00 1 ...form=fakeentry... 
Sólo URL con trans_sid y form=action001...form=action...
Sólo URL sin trans_sid000indiferente
Cookies o URL con trans_sid101...form=fakeentry...
Cookies o URL sin trans_sid100indiferenteUsando constante SID
Sólo POST000indiferenteIgual que sólo URL sin trans_sid, pero sólo usando POST

La razón de tantas posibilidades se debe a que el servidor, en este caso PHP, trata de cumplir el estándar RFC2965 sobre el mecanismo de estado de sesiones basadas en cookies, pues en su punto 2 expone que "Ni navegador ni servidor deben estar obligados a soportar cookies. Un servidor podría rehusar servir contenido a un navegador que no le devuelve una cookie que previamente le fue enviada." Por lo tanto si el navegador no acepta cookies, el servidor podría disponer de otras posibilidades para continuar usando sesiones. Aunque también se advierte que la forma más segura es usando sólo cookies, como veremos en un tema posterior sobre seguridad.

Esquema básico para todos los ejemplos

Se trata de elaborar un conjunto de 4 páginas para cada uno de los ejemplos anteriores:

    Una primera página llamada propaga0.php que será una página de entrada sin sesión. Tiene dos cometidos principales:
        Sirve de entrada y salida de la sesión. En las siguientes páginas con sesión si necesitáramos cerrar la sesión, nos remitiría aquí.
        Para la propagación con cookies, nos sirve para enviar una cookie para testear si el navegador las tiene activas. Esta cookie de prueba (que denominamos test-cookie) no es una cookie de sesión. En la siguiente página donde iniciamos sesión haríamos una consulta para ver si nos llegó esta cookie.
    Una segunda página propaga1.php que sería la página de inicio de sesión. Aquí hacemos el primer session_start() y damos valor por primera vez a una variable de sesión que llamaremos $_SESSION["iniciada"]. Así comprobaremos en las siguientes páginas que esta variable se está propagando con la sesión.
    Una tercera página propaga2.php que recogerá la variable de sesión. También se insertarán un par de formularios para ver cómo se reciben sus campos en la siguiente página.
    Una cuarta página propaga3.php que también recogerá la variable de sesión y los campos recibidos, en su caso, de la página anterior.

El código PHP de todos los ejemplos es similar, por lo que sólo se expone el código del ejemplo número 7 propagar SID con cookies o URL y con trans_sid, pues el más completo y el resto tiene muy pocas variaciones, en todo caso menos líneas de código. Las diferencias se exponen en cada apartado. Si lo desea puede descargar este archivo comprimido No tienes permitido ver los links. Registrarse o Entrar a mi cuenta (70 KB) con todos los códigos en formato de archivos de texto plano (con extensión cambiada a "txt").

Propagar SID: Sólo cookies

Este primer ejemplo puede verse desde el enlace solo-cookies/propaga0.php. La ejecución de las páginas presenta los estados de las variables y configuraciones relacionadas con la sesión que ayudarán a comprender mejor el proceso.

La página de entrada sin sesión propaga0.php sólo tiene un par de líneas de código PHP (en marrón). El HTML (en azul) que no tiene mayor interés lo obviamos. El JavaScript expuesto aquí, que se incluye en todas las páginas, busca las cookies en el navegador, lo que nos servirá para verificar que se están gestionando como se espera. Este script se expone con detalle en el tema I, en el apartado sobre Javascript también puede manejar cookies.

Código: php
<?php
define("SESION", "sesionCookie");
setcookie("test-cookie", SESION);
?>

...HTML...
    <code id="mi-cookie" style="border: red solid 1px;"> </code>
    <script>
        var arrayCookies = document.cookie.split(";");
        var contenido = "";
        for (var i=0; i<arrayCookies.length; i++){
            var unaCookie = arrayCookies[i].split("=");
            var nombre = quitaEspacios(unaCookie[0]);
            if ((nombre == "test-cookie")||
                (nombre == "<?php echo SESION; ?>")) {
               contenido += escapaHtml(arrayCookies[i]) + "; ";
            }
        }
        if (contenido == "") contenido = "No hay cookies en el navegador.";
        setInnerText(document.getElementById("mi-cookie"), contenido);
    </script>
...sigue HTML...


La constante SESION es simplemente para agilizar el uso del nombre de la sesión en el cuerpo del documento y, sobre todo, es más necesaria en el resto de páginas. La siguiente línea PHP si es importante en este caso. Dado que se trata de sesiones que sólo usarán cookies para propagarse, es necesario detectar si el navegador las tiene activadas. Como la sesión la iniciaremos en la siguiente página propaga1.php, en ese momento se habrá emitido una cookie de sesión al navegador, pero el servidor no sabría si fue aceptada. Así aprovechamos la página de entrada sin sesión para emitir una cookie de prueba (que llamamos test-cookie). Luego en la página de inicio de sesión comprobaremos que el navegador la aceptó si la recibimos.

La página de inicio de sesión es propaga1.php. El script PHP es ahora más extenso. En esta y las siguientes páginas ponemos lo mismo al inicio del código:

Código: php
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 1);
ini_set("session.use_trans_sid", 0);
define("SESION", "sesionCookie");
session_name(SESION);
session_start();


Las modificaciones en las opciones del php.ini sólo serán válidas durante la ejecución del script, por lo que habrá que acompañarlas en todas las páginas. A menos que podamos acceder al archivo php.ini y realizarlas ahí. Si nuestro sitio está en un alojamiento compartido, quizás esto no sea posible, pero incluso puede resultar una medida positiva la ejecución de los ini_set() en cada script, por si alguién le da por modificar el php.ini del alojamiento compartido.

Se trata de establecer las opciones para usar sólo cookies, como vimos en la tabla del primer apartado. Para cada ejemplo usamos un nombre distinto de sesión, siendo en este caso sesionCookie. Luego iniciamos sesión. A continuación vemos si hemos recibido la cookie de prueba:

Código: php
$hay_cookie = (((isset($_COOKIE["test-cookie"]))&&
    (($_COOKIE["test-cookie"]) == SESION))||
    (isset($_COOKIE[SESION])));
if ($hay_cookie) setcookie("test-cookie", "", time() - 42000);


Si es la primera vez que entramos en esta página con sesión no iniciada, no existirá $_COOKIE[SESION], por lo que tendremos que comprobar $_COOKIE["test-cookie"] que viene desde la página de entrada. Si $hay_cookie es true entonces el navegador tiene las cookies activadas y podemos eliminar la cookie de prueba. El código sigue con una acción para finalizar la sesión y llevarnos a la página de entrada sin sesión (propaga0.php):

Código: php
if (isset($_POST["fin-sesion"])){
    $_SESSION = array();
    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params["path"], $params["domain"],
            $params["secure"], $params["httponly"]
        );
    }
    session_destroy();
    $host  = $_SERVER["HTTP_HOST"];
    $uri   = rtrim(dirname($_SERVER["PHP_SELF"]), "/\\");
    $pagina = "propaga0.php";
    header("Location: http://$host$uri/$pagina");
    exit;
}


Este código para finalizar sesión ya se expuso en el tema II, en el apartado sobre destrucción de sesiones, por lo que no comentamos más nada aquí. Finalmente el script PHP acaba registrando la variable de sesión por primera vez:
Código: php

if (!isset($_SESSION["iniciada"])) {
    $_SESSION["iniciada"]  = "SESION INICIADA CON SOLO COOKIES";
}


Esta variable será la que nos indique en los siguientes accesos si se está propagando la sesión. Las siguientes páginas, propaga2.php y propaga3.php no tienen estas últimas líneas, mientras que el resto es igual a excepción de la verificación de cookies activadas. En este caso sólo basta con verificar que se ha recibido la cookie de sesión:

Código: php
$hay_cookie = isset($_COOKIE[SESION]);


Con el ejemplo en ejecución pueden verse los detalles acerca de las variables y configuraciones. Puede probarse desactivando las cookies del navegador y ver que la sesión no se propaga por las páginas, creando una nueva sesión en cada acceso.

Propagar SID: Sólo URL con trans_sid y form=fakeentry

La ejecución se inicia en este enlace solo-url-transsid-fake/propaga0.php. El código de la página de entrada sólo contiene la definición de la constante SESION. No es necesario enviar una cookie de prueba pues no se utilizarán para las sesiones. En la página de inicio de sesión, propaga1.php hacemos lo mismo que antes, pero adaptando la configuración a sólo URL con SID transparente y fakeentry para los formularios (ver tabla del primer apartado):

Código: php
ini_set("session.use_cookies", 0);
ini_set("session.use_only_cookies", 0);
ini_set("session.use_trans_sid", 1);
ini_set("url_rewriter.tags", "a=href,form=fakeentry");
define("SESION", "sesionUrlTranssidFake");
session_name(SESION);
session_start();
if (isset($_POST["fin-sesion"])){
    ...finalizamos sesión....

if (!isset($_SESSION["iniciada"])) {
    $_SESSION["iniciada"]  = "SESIÓN INICIADA CON SÓLO URL,
    TRANSSID Y FORM=FAKEENTRY";
}


La única diferencia es que no detectamos ninguna cookie, pues ahora no hace falta. Observamos el nuevo nombre de sesión que usaremos en este ejemplo sesionUrlTranssidFake. Además ahora especificamos la reescritura de tags a=href y form=fakeentry. Esta página de entrada de sesión tiene dos vínculos. Por un lado un formulario con un botón para finalizar sesión, cuyo vínculo en el atributo action la llama a si misma. Luego hay otro vínculo que apunta a la siguiente página propaga2.php. Si ejecutamos el ejemplo y vemos el código fuente generado en nuestro navegador, observaremos esto:

Código: php
...
<form action="propaga1.php" method="post">
<input type="hidden"
name="sesionUrlTranssidFake"
value="jbspfclsdtfhm57fe104n3r8a6" />
<input name="fin-sesion" type="submit" value="finalizar sesión" />
</form>
....
<li>Vínculo a otra página
<a
href="propaga2.php?sesionUrlTranssidFake=jbspfclsdtfhm57fe104n3r8a6">
propaga2.php</a></li>
...


Si observamos el vínculo <a>, vemos que PHP ha agregado el parámetro SID, es decir, nombre_sesión = identificador_sesión. Por otro lado PHP ha generado un elemento <input type="hidden" > en el formulario para albergar el SID de la sesión.

Esto lo hace PHP de forma transparente para el programador, teniendo en cuenta la configuración establecida en url_rewriter.tags, cuyo valor por defecto es a=href,area=href,frame=src,input=src,form=fakeentry,fieldset=, pero que hemos reescrito sólo para a=href,form=fakeentry pues el resto no los vamos a utilizar en este ejemplo. Así para los elementos <a> usará el agregado de parámetro en el atributo href, mientras que para los formularios la palabra clave fakeentry le dice que debe crear un campo oculto para ubicar el SID.

Si nos situamos en el vínculo que lleva a la siguiente página propaga2.php comprobaremos que la barra de estado del navegador contiene la URL con el parámetro de sesión. Al pasar a esa página observamos, en el apartado de variables de sesión, que ésta se recibió con $_GET["sesionUrlTranssidFake"]: jbspfclsdtfhm57fe104n3r8a6. Es decir, se recibe el parámetro con el SID por GET en la URL. Pero también lo podemos recibir por POST.

En esa misma página propaga2.php tenemos un formulario que remite por POST su campo a la página propaga3.php. Si lo ejecutamos veremos en el apartado de variables de sesión lo siguiente:
Código: php

$_GET["sesionUrlTranssidFake"]: NO EXISTE
$_POST["sesionUrlTranssidFake"]: jbspfclsdtfhm57fe104n3r8a6
$_GET["campo-get"] del formulario GET: NO EXISTE
$_POST["campo-post"] del formulario POST: Valor del input post

   

En definitiva, PHP estará vigilando los GET y POST para el recibido de un parámetro con el nombre de la sesión. Entonces lo compara con las sesiones iniciadas y si el identificador coincide podrá completar el proceso de identificación de sesión tal como hacía en el caso de cookies.

Si tenemos un vínculo a un sitio externo, PHP no agrega el SID. Por ejemplo, en la última página propaga3.php tenemos estos tres vínculos generados en nuestro navegador:

Código: php
...
<li>Volver a la primera página
<a href="propaga1.php?sesionUrlTranssidFake=jbspfclsdtfhm57fe104n3r8a6">
propaga1.php</a></li>
<li>Volver a la segunda página
<a href="propaga2.php?sesionUrlTranssidFake=jbspfclsdtfhm57fe104n3r8a6">
propaga2.php</a></li>           
<li>Enlace a un sitio externo
<a href="http://docs.php.net/manual/es/">http://docs.php.net/manual/es/</a>
</li>       
...


PHP agregó el SID a los dos primeros mientras que no lo hizo con el último, supongo porque detectó http:// al inicio. De todas formas dejar en manos de PHP que realice estas acciones puede comprometer la seguridad al exponer un SID con una URL a un sitio externo, en el caso de que por alguna razón algo no vaya bien. Luego veremos que podemos mantener un mayor control sin utilizar use_trans_sid, aunque suponga más esfuerzo pues habrá que escribir todos los agregados de SID.

Propagar SID: Sólo URL con trans_sid y form=action

La ejecución se inicia en este enlace solo-url-transsid-action/propaga0.php. Este ejemplo es igual que el anterior, propagando la sesión sólo con URL, pero usando el atributo action en la opción url_rewriter.tags en lugar de fakeentry. Las configuraciones iniciales son:

Código: php
ini_set("session.use_cookies", 0);
ini_set("session.use_only_cookies", 0);
ini_set("session.use_trans_sid", 1);
ini_set("url_rewriter.tags", "a=href,form=action");
define("SESION", "sesionUrlTranssidAction");
session_name(SESION);
session_start();


Al solicitar la página propaga2.php podemos ver el código fuente generado en nuestro navegador:

Código: php
...
<form
action="propaga2.php?sesionUrlTranssidAction=1375aptlieg4arkrsm0rfjpj26"
method="post">
<input type="hidden" name="sesionUrlTranssidAction"
value="1375aptlieg4arkrsm0rfjpj26" />
<input name="fin-sesion" type="submit" value="finalizar sesión" />
</form>
.....
<a href="propaga1.php?sesionUrlTranssidAction=1375aptlieg4arkrsm0rfjpj26">
propaga1.php</a>
...
<form action="propaga3.php?sesionUrlTranssidAction=1375aptlieg4arkrsm0rfjpj26"
method="get">
<input type="hidden" name="sesionUrlTranssidAction"
value="1375aptlieg4arkrsm0rfjpj26" />
<label>campo-get:
<input type="text" name="campo-get" value="Valor del input get" />
</label>
<input type="submit" value="enviar" />
</form>
...
<form
action="propaga3.php?sesionUrlTranssidAction=1375aptlieg4arkrsm0rfjpj26"
method="post">
<input type="hidden" name="sesionUrlTranssidAction"
value="1375aptlieg4arkrsm0rfjpj26" />
...


En los elementos formulario vemos que PHP agregó el SID en el atributo action de la misma forma que para los elementos vínculo, pero también dotó a los formularios del campo de texto oculto. Si desde esta página propaga2.php envíamos el formulario POST, podemos observar en propaga3.php este resultado:

Código: php
$_GET["sesionUrlTranssidAction"]: 1375aptlieg4arkrsm0rfjpj26 
$_POST["sesionUrlTranssidAction"]: 1375aptlieg4arkrsm0rfjpj26
$_GET["campo-get"] del formulario GET: NO EXISTE
$_POST["campo-post"] del formulario POST: Valor del input post

   

Se recibe el SID por ambas vías, el GET del parámetro URL agregado al action del formulario y el POST del campo oculto.

Propagar SID: Sólo URL sin trans_sid

La ejecución se inicia en este enlace solo-url-no-transsid/propaga0.php. Las configuraciones iniciales son:

Código: php
ini_set("session.use_cookies", 0);
ini_set("session.use_only_cookies", 0);
ini_set("session.use_trans_sid", 0);
define("SESION", "sesionUrlNoTranssid");
session_name(SESION);
session_start();


Ahora el programador debe incluir los SID manualmente en cada vínculo. Por ejemplo, la página propaga2.php tiene los elementos con vínculos con el SID de esta forma antes de que el script se procese, donde en amarillo aparece resaltado lo que hemos tenido que agregar en el código:

Código: php
...
<a href="propaga1.php?<?php echo SESION."=".session_id(); ?>">
propaga1.php</a></li>   
...
<form action="propaga3.php" method="get">
<input type="hidden" name="<?php echo SESION; ?>"
value="<?php echo session_id(); ?>" />
<label>campo-get:
<input type="text" name="campo-get" value="Valor del input get" />
</label>
<input type="submit" value="enviar" />
</form>
...
<form action="propaga3.php" method="post">
<input type="hidden" name="<?php echo SESION; ?>"
value="<?php echo session_id(); ?>" />
<label>campo-post:
<input type="text" name="campo-post" value="Valor del input post" />
</label>
<input type="submit" value="enviar" />
</form>
...


Como hemos mencionado en comentarios anteriores, PHP dispone de la constante SID que se actualiza después de hacer un session_start() con el mismo resultado que la cadena session_name()."=".session_id(). La ventaja es que si estamos propagando por cookies, esta constante será una cadena vacía, por lo que podemos usar este método con un ejemplo que soporte propagación por cookies o URL sin trans_sid, como veremos en un apartado más abajo. En el ejemplo actual podríamos usar esa constante para no tener que estar escribiendo nombre e identificador, pero esto sólo nos sirve para los vínculos URL, pues en los campos ocultos de formulario tenemos que ponerlo por separado.

En el código resultante en el navegador veremos esto para una sesión en ejecución:

Código: php
...
<a href="propaga1.php?sesionUrlNoTranssid=4uuunt9u3jtebkr9afscpkrvp1">
propaga1.php</a>
...
<form action="propaga3.php" method="get">
<input type="hidden" name="sesionUrlNoTranssid"
value="4uuunt9u3jtebkr9afscpkrvp1" />
...
</form>
...
<form action="propaga3.php" method="post">
<input type="hidden" name="sesionUrlNoTranssid"
value="4uuunt9u3jtebkr9afscpkrvp1" />
...
</form>
...


Cuando veamos los temas de seguridad, comprenderemos que no es conveniente propagar el SID por URL, es decir, vía GET. Así en este ejemplo se propaga por POST sólo para el formulario con method="post", pues para los que tienen method="get" y para los vínculos <a> lo hará por GET. Podemos obligarnos sólo a incluir formularios POST, pero los vínculos <a> sólo podrán propagar la sesión como un parámetro URL (aunque al final del tema veremos una forma de evitarlo).

Propagar SID: Cookies o URL con trans_sid

La ejecución se inicia en este enlace cookies-url/propaga0.php. En este ejemplo PHP usará cookies si están activadas y en otro caso agregará el SID a los vínculos. El inicio del PHP de las páginas de sesión es:

Código: php
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 0);
ini_set("session.use_trans_sid", 1);
ini_set("url_rewriter.tags", "a=href,form=fakeentry");
define("SESION", "sesionCookieUrl");
session_name(SESION);
session_start();   
...


El ejemplo es exactamente igual que el del primer apartado con sólo cookies, pero cambiando este inicio de configuraciones. El use_trans_sid se encarga de agregar los SID a los vínculos de forma transparente. Así parece que este ejemplo consigue evitar la desactivación de las cookies por parte del usuario, asegurando la propagación de la sesión en todos los casos.

Cuando se propaga por cookies sucederá que en la página de inicio de sesión propaga1.php, sólo la primera vez que se inicia una sesión, los vínculos de esa página propagarán por URL, aparte de que también envíe la cookie. Esto sucede porque PHP no ha recibido aún alguna cookie de sesión y decide enviar el SID también por URL. Esto lo podemos ver en el código fuente generado en nuestro navegador en esa primera entrada a la página:

Código: php
...
Vínculo a otra página
<a href="propaga2.php?sesionCookieUrl=fttcg55uddc4oi0qvshqc27bs6">
propaga2.php</a>
...


Mientras que si volvemos a esa misma página propaga1.php después de haber estado en otra, o bien simplemente la recargamos, entonces ya no aparecerá el parámetro del vínculo, pues PHP ya habrá recibido la cookie de sesión del navegador.

Propagar SID: Cookies o URL sin trans_sid y usando constante SID

La ejecución se inicia en este enlace cookies-url-sid/propaga0.php. El ejemplo es igual que el del apartado anterior cookies o url, pero ahora sin utilizar trans_sid. Las configuraciones iniciales son:

Código: php
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 0);
ini_set("session.use_trans_sid", 0);
define("SESION", "sesionCookieUrlSid");
session_name(SESION);
session_start();


La constante SID es construida por PHP concatenando session_name()."=".session_id(). La única ventaja es que usando un modo de configuración con propagación por cookies-url como el de este ejemplo, si PHP detecta que recibe sesión por cookie entonces esa constante será una cadena vacía. En otro caso se construye como hemos dicho. Así podemos incorporar la constante en nuestro código y no preocuparnos cuando la propagación sea por cookies pues no se agregará el parámetro de sesión en la URL. Por ejemplo, la página propaga2.php, la parte de vínculos y formularios sería escrita como sigue:

Código: php
...    
<a href="propaga1.php?<?php echo SID; ?>">propaga1.php</a></li>   
...
<a href="propaga3.php?<?php echo SID; ?>">propaga3.php</a></li>
...
<!--  En el caso de un form GET no podemos agregar el SID en el
action, pues en el envío el navegador lo sustituye por los campos
que va a enviar -->
<form action="propaga3.php" method="get">
    <input type="hidden" name="<?php echo SESION; ?>"
    value="<?php echo session_id(); ?>" />
    <label>campo-get:
    <input type="text" name="campo-get" value="Valor del input get" />
    </label>
    <input type="submit" value="enviar" />
</form>
...
<form action="propaga3.php?<?php echo SID; ?>" method="post">
    <label>campo-post:
    <input type="text" name="campo-post" value="Valor del input post" />
    </label>
    <input type="submit" value="enviar" />
</form>   
...


Cuando las cookies esten activadas, entonces los vínculos no contienen los parámetros pues la constante SID será una cadena vacía (""). Cuando estén desactivadas esa constante será el SID que se agregará como parámetro. Puede probar el ejemplo activando y desactivando cookies y observando el código fuente generado en su navegador.

El comentario HTML hace referencia a que si agregamos el parámetro en el formulario GET, en el envío del mismo parece sustituirlo por lo parámetros que se remiten. Por eso ahí se incluye como campo oculto.

Debe observarse que este ejemplo tiene el mismo problema que comentamos en los párrafos finales del apartado anterior, pues usando cookies, en el primer inicio de sesión agrega el SID en la URL dado que aún no ha recibido cookie de sesión.

Propagar SID: sólo POST

También es posible no enviar el SID por URL ni mediante cookies, sino únicamente usando campos ocultos en formularios POST. Toda petición que se haga al servidor, incluso la de vínculos <a>, se realizará con formularios POST. De esta forma no dependemos de que el usuario tenga activadas las cookies y, por otro lado, no exponemos el SID en las URL. Sin embargo sería necesario que el navegador tenga activado JavaScript. La ejecución se inicia en este enlace solo-post/propaga0.php. El ejemplo es igual que el del apartado sólo URL sin trans_sid, con las mismas configuraciones iniciales:

Código: php
ini_set("session.use_cookies", 0);
ini_set("session.use_only_cookies", 0);
ini_set("session.use_trans_sid", 0);
define("SESION", "sesionPost");
session_name(SESION);
session_start();


Entonces la propagación de SID hemos de realizarla manualmente en el código. Pero nos obligaremos a que toda clase de vínculos se realice mediante un formulario POST. Así sólo tendremos formularios con method="post" y en ningún caso usaremos el method="get". En cada formulario agregaremos el campo oculto con el SID.

Para todos los vínculos <a> de una página usaremos un único formulario que podemos disponer antes de la aparición del primero de los vínculos. Lo completaremos con el campo oculto que porta el SID:

Código: php
<form action="" method="post" id="form-vinculos">
    <input type="hidden" name="<?php echo SESION; ?>"
    value="<?php echo session_id(); ?>" />   
</form>


Note que el atributo action es una cadena vacía. Al no contener elementos visibles, este formulario no aparecerá en pantalla, aunque en el ejemplo en ejecución le agregamos un borde rojo para poder apreciarlo. Luego todos los vínculos de la páginas los escribiremos de esta forma:

Código: php
<a href="javascript:irA('propaga2.php');">propaga2.php</a>


El destino del vínculo será el argumento de la función irA(), que se ejecutará mediante script de JavaScript que ubicaremos en el encabezado de cada página:

Código: php
<script>
    function irA(destino){
        var formulario = document.getElementById("form-vinculos");
        formulario.action = destino;
        formulario.submit();
    }
</script>


Este script identifica el formulario de vínculos y le dota del destino al atributo action. Luego el método submit() de JavaScript permite remitir el formulario como si hubiésemos pulsado un botón con tipo submit.

Las ventajas de este ejemplo es que no dependemos de las cookies y no exponemos el SID en la URL. Por contra se basa en que el usuario tenga activado JavaScript. Otro problema es que cuando el usuario se sitúa sobre un vínculo no verá el destino en la barra de estado del navegador, sino el literal javascript:irA('un_destino');. Esto puede condicionar al usuario pues no se le está ofreciendo una cadena URL clásica, aunque en ese texto aún puede ver el destino al que se dirige.

Asegurar sesiones

Suplantación de sesiones

El tema de seguridad en la web es muy extenso y complejo. Requiere un profundo conocimiento del funcionamiento de todos los aspectos relacionados: servidores, navegadores, protocolos de comunicaciones, redes, etc. En esta página no pretendo mostrar un estudio exhaustivo, sino lo básico para empezar a enfrentarnos con este problema en relación con las sesiones. Veremos dos conceptos que se repiten mucho en lo que he leído: session hijacking y session fixation.

En los temas anteriores hemos visto para que sirve una sesión y cómo trabaja. Hemos indicado algunas cuestiones relacionadas con la seguridad de las sesiones. Es obvio que las sesiones sirven para algo más que lo que vimos en los ejemplos de los temas anteriores. Son especialmente útiles en el control de accesos a ciertos recursos protegidos de nuestro sitio, estableciendo un "canal" único entre el servidor y un determinado cliente.

Las sesiones se basan en la existencia del identificador de sesión que emplea el servidor para establecer la comunicación. Ese identificador está presente en el servidor y en el navegador, viajando por la red en uno y otro sentido con las peticiones y respuestas. Si en un momento dado hay una sesión abierta, un atacante que pueda hacerse con su identificador podría acceder al servidor sin que éste o el cliente se apercibieran, a no ser que hagamos algo para evitarlo.

El uso no deseado de un identificador válido de otro usuario es lo que se conoce como secuestro de sesión (session hijacking). Podemos decir que un indentificador válido es aquel que fue generado por un usuario legítimo mediante un script PHP y que identifica su sesión activa. Las sesiones siguen activas mientras no se eliminen de la carpeta Temp, por lo que incluso después de finalizar su vida podrían seguir activas si aún no han sido eliminadas por el mecanismo de control se sesiones. Hay tres formas de obtener un identificador de sesión válido:

    Interceptando un identificador donde se almacene o fluya. Ya hemos visto que en un alojamiento compartido hay un riesgo de que otros sitios alojados puedan leer nuestros identificadores de sesión. En la propia red se pueden interceptar aunque para evitarlo se usan comunicaciones cifradas con el protocolo HTTPS. E incluso pueden leerse identificadores en el navegador del usuario.
    Prediciendo cuál será el identificador de sesión para un usuario determinado. En PHP el identificador de sesión es un valor aleatorio que, según parece, no es fácil de predecir.
    Usando la "fuerza-bruta" para iterar por todos los identificadores posibles hasta que demos con uno igual que alguno que se esté usando en ese momento. Como la longitud de los identificadores es mucho mayor que el número de sesiones simultáneas, parece poco probable que esto tenga éxito.

El objetivo final del atacante es suplantar al usuario, pues una vez que obtenga una sesión válida, podrá acceder a los mismos recursos del servidor que ese usuario. Hay otra forma de suplantar sesiones sin necesidad de conocer un identificador válido. Se trata en este caso de que el atacante emita un identificador previamente a que el usuario haya entrado en el sitio. El atacante entonces intentará que el usuario entre en el sistema con este identificador. Esto se le denomina fijación de sesión (session fixation), pues de alguna forma el atacante fija la sesión antes de que el usuario entre, a diferencia del secuestro, donde el atacante "roba" la sesión después de que el usuario haya entrado. En sentido general a veces decimos suplantación de sesión cuando en realidad queremos referirnos al hecho de suplantar a un usuario en el uso de una sesión, ataques que pueden conseguirse con las técnicas descritas del secuestro y la fijación.

Para realizar un ejemplo de suplantación y exponer los conceptos señalados de fijación y secuestro de sesión, realizamos en el siguiente apartado unas páginas de ejemplo de sesión con autenticación.

Ejemplo de sesión con autenticación

Es obvio que un ataque de sesión se llevará a cabo si el atacante obtiene algún beneficio. Un caso típico es poder acceder a recursos protegidos en el sitio sin que quede huella de su paso. El acceso a estos recursos suele requerir que el usuario canalizado por medio de una sesión sea también autenticado, es decir, sea identificado por medio de la comparación con una información de usuario que previamente tendremos almacenada en nuestro sitio. Por lo tanto hemos de evitar que el atacante suplante una sesión con autenticación, pues le permitirá acceder a esos recursos haciéndose pasar por el usuario legítimo.

El proceso de autenticación o autentificación en un sistema web podemos resumirlo como el modo de asegurar que el usuario que se conecta es quién realmente dice ser. Para ello debemos disponer en nuestro sitio de una copia de la contraseña para compararla con la que el usuario nos envía. Porque partimos de la base de que esa contraseña sólo será conocida por el servidor y el cliente. La autenticación no sustituye a la sesión, pues con una sesión abrimos un canal de comunicación con un único cliente, aspecto necesario para saber que sólo le estamos sirviendo a este usuario que inició la sesión y no a otro. Pero no podemos saber quién es ese usuario, para lo cuál necesitamos la autenticación.
En inglés se usa el término "login" (de "log in") traducido como "entrar o registrarse en un sistema" en un contexto informático, pues el verbo "log" significa registrar. De la misma forma "logout" para la acción de "salir de un sistema". He visto que se usa el verbo en español "loguear" como "autenticar", pero no está admitido en el R.A.E., al menos por ahora. Probablemente el clásico botón de "login" será más asequible para los usuarios no expertos que uno con la leyenda "autenticar" o "autentificar".

Además la autenticación da lugar dos figuras necesarias en un proceso de comunicación. Por una lado la autorización, proceso por el cual el servidor autoriza al usuario autenticado a acceder a ciertos recursos y luego tenemos la auditoría, con lo que el servidor registra los accesos de un usuario autenticado para resolver posibles incidencias.

El proceso de autorización se entiende mejor si tenemos planificado el acceso a los recursos en grupos según niveles de protección de esos recursos. Entonces el usuario de un cierto grupo que se autentique será autorizado a acceder sólo a los recursos a que tenga derecho. Cuando todos los recursos están al mismo nivel, no hay diferencia entre autenticación y autorización, pudiéndose prescindir de este último pero entendiendo que el proceso está implícito. Por otro lado, la auditoría nos permite realizar una investigación posterior de las incidencias que se hayan ocasionado en los accesos.

Para exponer un ejemplo lo más cercano a un caso real y luego aplicar los conceptos anteriores de fijación y secuestro de sesión, elaboramos un conjunto de 3 páginas con sesión y autenticación:

    autentica0.php: es una página de entrada sin sesión, donde se ofrece al usuario la posibilidad de entrar en una página con sesión, para autenticarse y acceder a recursos protegidos.
    autentica1.php: en esta página se inicia una sesión, ofreciendo al usuario un formulario de autenticación (nombre+contraseña), datos que de validarse le permitirán pasar a la siguiente página. También damos posibilidad para registrarse si no lo estuviera antes.
    autentica2.php: en esta página tenemos los recursos o activos de valor, es decir, la información protegida que sólo podrán ver los usuarios registrados.

También se dispone de una página con sólo html (acceso-indebido.html) para redirigir a un usuario cuando indebidamente intente acceder a la página protegida autentica2.php sin estar autenticado. En principio no es necesario conocer en profundidad el mecanismo de este conjunto de páginas para los apartados siguientes, pero si lo desea puede ampliar la información en el siguiente epígrafe (desplegable oculto) o bien ver todo el código completo del conjunto de páginas.

Las páginas PHP de este ejemplo disponen al inicio del script de unas variables específicas de control para poder usar los ejemplos de suplantación de sesiones:

    $solo_cookies (en todas las páginas PHP). Con valor true se realizará una propagación de SID con sólo cookies (ver propagar SID con sólo cookies). Con valor false propagaremos el SID con sólo URL (ver propagar SID con sólo URL sin trans_sid). Así se tratará de evidenciar el riesgo que tiene este método en las suplantaciones de sesión.
    $evita_fijacion (en autentica1.php). Con valor true se regenerará el identificador de sesión para evitar la fijación de sesiones, efecto que podrá comprobar con valor false.
    $evita_secuestro (en autentica1.php y autentica2.php). Con valor true realizará una identificación del usuario comprobando si su navegador es el mismo que usó cuando se inició la sesión. Con valor false podrá comprobarse el efecto del secuestro de sesión.

Ejecutando este ejemplo en línea desde este sitio estarán con valor true de forma permanente. Pero si queremos ver los efectos de la suplantación, hemos de ejecutar estas páginas en un dominio localhost para usar con un servidor Apache+PHP montado como local. Para ello puede descargar el conjunto de páginas comprimidas en No tienes permitido ver los links. Registrarse o Entrar a mi cuenta. Las extensiones de los archivos ".php" las he modificado por ".txt", por lo que una vez descargado y descomprimido hay que hacer el proceso inverso, aparte de también poner aquellas variables a false para ver el efecto de la suplantación.

La fijación de sesión (session fixation)

Si ejecuta este ejemplo en su localhost, recuerde poner las variables de control así para poder ver el efecto:

Código: php
$solo_cookies = false; (en las 3 páginas php)
$evita_fijacion = false; (en autentica1.php)
$evita_secuestro = false; (en autentica1.php y autentica2.php)


La fijación de sesión (session fixation) podemos definirla como la suplantación de usuario antes de que éste inicie sesión. El primer paso del atacante sería iniciar una sesión en el conjunto de páginas. Supongamos que esto lo hago en local con uno de los navegadores que tengo instalado en mi ordenador, como el Google Chrome. Después de ir a la página de entrada sigo el vínculo a la primera página autentica1.php y obtengo una pantalla con un formulario para autenticar:


En primer lugar el atacante necesita mantener una sesión viva en el servidor. Además debe obtener el identificador de sesión lo que puede hacer de varias formas. Puede ver el código fuente HTML en el navegador para comprobar si está embebido en los vínculos. En nuestro ejemplo lo va a encontrar como campo oculto de los formularios. Recuerde que estamos en localhost usando el valor false para la variable $solo_cookies, por lo que los formularios se envían por GET con campos ocultos para el identificador de sesión, pues la propagación es por URL:

Código: php
<form action="/como-se-hace/php-sesion/ejemplos/⇒
    sesion-registrada/autentica1.php"
method="get" style="border: gray solid 1px">
<input type="hidden" name="aseguraSesion"
value="r96ol6v2ecorf76gf72d0ne135" />
</form>


También podría enviar el botón de autenticar con el formulario vacío y el servidor le responde otra vez con el formulario, acompañando el SID en la ruta que se expone en la barra de direcciones del navegador Chrome, donde podría copiar el identificador:


Decíamos que debe mantener la sesión viva hasta conseguir el ataque. Recordemos que las sesiones tienen una duración de vida, por defecto de 24 minutos (ver tema II en el apartado de vida de una sesión). Si el usuario no interactúa con la sesión, esta se borra de la carpeta Temp si ya ha cumplido su vida útil desde la última interacción. El atacante puede mantenerla viva presionado periódicamente el botón de autenticar, aunque supuestamente usará algún programa que le permita esta acción de forma automática.

Mientras la sesión se mantiene viva, el atacante intentará que un usuario legítimo entre en el sistema usando su identificador. Debe entonces trasladarlo al navegador del usuario. Esta es la parte más compleja y caben varios métodos, incluso si se está usando propagación por cookies. En este caso estamos propagando por URL y es más sencillo presentar un ejemplo de traslado de identificador. Por ejemplo, supongamos que el atacante conoce de alguna forma el email del usuario. Podría enviarle uno con un mensaje HTML con este vínculo:

Tenemos una oferta atractiva para Usted. Si está interesado, por favor,
Código: php
<a href="http://localhost/como-se-hace/php-sesion/ejemplos/⇒
    sesion-registrada/autentica1.php?aseguraSesion=r96ol6v2ecorf76gf72d0ne135">
entre en el sistema</a>


El atacante presentaría el mensaje con lo necesario para engañar al cliente quién podría ya estar acostumbrado a recibir correos electrónicos del sitio donde esta registrado. Para emular en nuestro servidor local el acto del usuario pulsando sobre ese vínculo recibido en su email, copiamos el URL y lo trasladamos a la barra de direcciones de otro navegador, en este caso el Internet Explorer (este será supuestamente el navegador del usuario):

Esto le conduce al usuario a entrar en una sesión iniciada previamente por el atacante, comprobándose que el identificador de sesión es el mismo que inició el atacante:


El usuario no piensa que está siendo engañado, pues realmente está en la página del sitio. Así que se autenticará introduciendo su nombre de usuario y contraseña, tras lo cual el script le devolverá el vínculo con la página de recursos protegidos:


CitarEs evidente que por mucho que hagamos para protegernos la fijación de sesión, no estaremos conseguiendo nada si el atacante es capaz de robar el nombre de usuario y la contraseña que viaja sin cifrar por la red. Para evitarlo se usan protocolos seguros, pero aún así es necesario protegerse de la fijación.

A partir de ese momento, en algúno de los procesos automáticos que realiza el atacante pulsando el botón de autenticar, si el usuario ya se autenticó, entonces el atacante podrá entrar. Esto es lo que vemos si ahora pulsamos ese botón en el navegador Chrome, permitiéndole pasar a la página de recursos protegidos autentica2.php sin necesidad de conocer la contraseña de acceso:


Si en lugar de propagar por URL usáramos sólo cookies podríamos mitigar el problema pero no desaparecería. El atacante tendría que llevar una cookie con su identificador hasta el navegador del usuario, pero es factible usando diversas técnicas, entre ellas y sólo a título informativo:

    Usando JavaScript en el navegador del usuario para emitir la cookie. Para ello puede servirse de cross-site scripting (XSS) que consiste en aprovechar vulnerabilidades del código HTML del sitio para incrustar JavaScript y hacer un document.cookie="aseguraSesion=r96ol...".
    Inyectando la etiqueta HTML <meta http-equiv=Set-Cookie content="aseguraSesion=r96ol..."> con alguna vulnerabilidad del servidor.

Afortunadamente PHP dispone de un mecanismo para evitar la fijación de sesión en cualquiera de las dos formas de propagación del identificador. En nuestro ejemplo, cuando el usuario engañado por el email entra en la página autentica1.php ya hay un identificador previo creado por el atacante. De hecho es una sesión completamente válida, con su archivo de sesión almacenado en Temp. Pero PHP no sabe que proviene de un ataque. Lo que podemos hacer es cambiar de identificador cuando el usuario sea autenticado. Eso se hace con la la función session_regenerate_id(true). Entonces PHP cambia el identificador y traspasa el contenido de las variables de esa sesión al nuevo, eliminando el archivo de sesión anterior. Ahora el usuario ya autenticado tiene un nuevo identificador, de hecho una nueva sesión, por lo que el atacante no podrá usarla. Si realiza el mismo proceso que antes, verá que tras la autenticación del usuario se genera un nuevo identificador. Luego en el otro navegador del atacante ya no será válido el identificador anterior, de hecho, no existirá y se le generará uno nuevo cuando pulse "autenticar".

Cuando con PHP hacemos session_start() y de alguna forma esa página ya porta un identificador de sesión entonces PHP lo adopta, independientemente de que lo haya generado anteriormente. Es lo que se llama un sistema permisivo, pues adopta cualquier identificador si no hay uno creado previamente. En oposición están los sistemas estrictos que sólo aceptan identificadores conocidos que fueron generados por el sistema en algún momento del pasado (como hace IIS de Microsoft). En los sistemas permisivos también existe la posibilidad de enviar un falso identificador con sesiones no inicidas. El atacante puede iniciar una sesión con un identificador como "1234" por ejemplo. Poniendo en la barra de direcciones del Chrome (navegador del atacante) esto:

Código: php
http://localhost/temas/php-sesion/ejemplos/⇒
    sesion-registrada/autentica1.php?aseguraSesion=1234


PHP genera una nueva sesión con ese identificador. El resto del proceso es igual que antes, enviarlo al usuario y esperar que este se autentique para tener luego acceso a esta sesión (siempre que no se regenere el identificador después de la autenticación).

Podemos evitar los falsos identificadores con sesiones no iniciadas si asociamos cada inicio de sesión con una variable de sesión que nos sirva para controlar el estado de la misma. Así usando una variable como $_SESSION["estado"], cuando el atacante entre por primera vez para registrar su falso identificador esta variable no existirá para ese identificador. Esto lo podemos ver en el diagrama de flujo, donde preguntamos si "¿Hay sesión iniciada?", que equivale a observar si la variable de sesión ha sido establecida: isset($_SESSION["estado"]). Entonces podemos hacer que PHP regenere el identificador. A partir de este momento el atacante se verá obligado a usar un identificador generado por PHP, que junto al método de regeneración tras la autenticación podrá evitar el ataque de fijación.

La norma general es que cuando se pase de un nivel de seguridad o otro más restrictivo, como puede ser una autenticación, se aconseja regenerar el identificador. No es bueno regenerarlo con excesiva frecuencia, por ejemplo con cada petición de una página, pues PHP estará ejecutando esa función y con muchos usuarios podrá recargar el sistema.

Secuestro de sessión (session hijacking)

Si la fijación de sesión es más díficil de entender que de solucionar, para el secuestro el problema se invierte. Secuestrar una sesión es suplantar a un usuario que ya ha entrado al sistema y por lo tanto existe una sesión activa. Las formas de "robar" su identificador de sesión se expusieron en el primer apartado de este tema. El problema es que si un atacante conoce el identificador después de ser regenerado tras la autenticación del usuario, nada le impide realizar una petición haciéndose pasar por el usuario legítimo.

En el ejemplo anterior usábamos propagación por URL que sabemos que tiene riesgos como cuando las páginas se almacenan en cachés con el parámetro del identificador. Si aún tenemos el ejemplo abierto con una sesión autenticada en autentica1.php, podemos pasar a la página protegida autentica2.php y copiar la URL de la barra de direcciones:
Código: php

http://localhost/temas/php-sesion/ejemplos/sesion-registrada/⇒
    autentica2.php?aseguraSesion=r96ol6v2ecorf76gf72d0ne135


Luego la trasladamos a otro navegador diferente y observará que podemos acceder también a esa página protegida, como hemos experimentado con el navegador Safari:


De igual forma podría hacer cualquiera si encuentra esa dirección almacenada en una caché, como la que usan los buscadores. Esto se mitiga usando propagación con sólo cookies, pues el identificador únicamente viaja en una cookie y no se expone en la URL. Pero usar sólo cookies no evita este secuestro, pues el atacante puede aún robar una cookie de sesión en el navegador del usuario o en su fluir por la red.

Para verificar que es posible el secuestro de sesiones con cookies, podemos realizar esta suplantación en nuestro localhost. Para ello ponemos las variables de control del ejemplo de esta forma:

Código: php
$solo_cookies = true; (en las 3 páginas php)
$evita_fijacion = true; (en autentica1.php)
$evita_secuestro = false; (en autentica1.php y autentica2.php)


Luego vamos a un navegador, por ejemplo Internet Explorer y, desde una sesión finalizada, iniciamos autentica0.php, luego seguimos el enlace a autentica1.php para autenticarnos y pasar a la página protegida autentica2.php. Copiamos la dirección de la barra

Código: php
http://localhost/temas/php-sesion/ejemplos/⇒
    sesion-registrada/autentica2.php

y los datos de la sesión que aparece en los resultados:

Código: php
session_name(): aseguraSesion 
session_id(): i6p1b3ujomn9vqj4738t7nbta0


Hemos de imaginar que el atacante logró este SID de alguna otra forma, pero sea cual sea ahora podrá acceder a esa página en su navegador, supongamos el Google Chrome. Para ello puede usar una aplicación telnet con php y enviar esta petición al servidor:

Código: php
GET /temas/php-sesion/ejemplos/sesion-registrada/⇒
    autentica2.php HTTP/1.1
Host: localhost
cookie: aseguraSesion=i6p1b3ujomn9vqj4738t7nbta0
Connection: Close


El servidor le responderá con el HTML de la página protegida autentica2.php, cuyo código verá en ese telnet-php:

Código: php
HTTP/1.1 200 OK
Date: Sun, 07 Nov 2010 11:59:56 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 4869
Connection: close
Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    <title>autentica2.php</title>

    <p>Ha entrado en este sitio <big><big>PROTEGIDO</big></big>
    con su nombre usuario: <big style="color: green">user</big>.
    </p>

    <li><code>$_COOKIE["aseguraSesion"]</code>:<code class="verde">
    i6p1b3ujomn9vqj4738t7nbta0</code></li>
    ...


Se observa que el atacante puede acceder a la página protegida enviando la cookie de sesión desde el telnet-php. Es importante observar el encabezado de la respuesta, con un valor 200 que significa en el protocolo HTTP que la respuesta ha finalizado con éxito. Por otro lado hemos ejecutado ese telnet-php en el navegador Chrome pero la cookie se envió desde telnet-php, no desde el navegador, por lo que no está almacenada ahí. Podemos comprobar en el navegador Chrome que no hay ninguna cookie de localhost almacenada. De hecho el PHP en el servidor la recibe pensando que proviene de un navegador que la tiene almacenada y que se generó en algún momento anterior al iniciar una sesión. En la repuesta no vuelve a enviar la cookie de sesión pues en la petición comprobó que el identificador coincidía con la que tenía almacenada en una sesión.

En cambio si volvemos a repetir el experimento pero antes poniendo la variable $evita_secuestro a true en las páginas correspondientes veremos que se puede intentar evitar. El procedimiento en este caso es almacenar una variable que identifique al usuario:

Código: php
$_SESSION["identifica-usuario"] = md5($_SERVER["HTTP_USER_AGENT"])


Se trata de guardar la cabecera que envían los navegadores con su información, el User-Agent que viene a ser el agente de usuario que envío la petición. Esto lo hacemos en el inicio de sesión y con cada petición del usuario comprobamos que el navegador es el mismo. Hay otras cabeceras como Accept o incluso almacenar la IP del usuario, pero parece ser que no son tan fiables pues el usuario puede estarse conectando mediante mecanismos que modifican estas cabeceras, como los proxys, o incluso cuando las IP son dinámicas y pueden ser modificadas durante el transcurso de varias peticiones. Parece que incluso el User-Agent podría verse alterado en ciertos casos, no sirviendo tampoco para este cometido.

El tema no lo tengo muy claro en este aspecto. Sea como fuere vamos a utilizar el User-Agent como identificador de usuario. Probamos esta posibilidad finalizando la sesión actual en el Explorer (navegador del usuario) y volviendo a autenticarnos para llegar luego a la página protegida. Esta sesión ha generado el identificador o9o496qi2oo6abedgti31gedb3. En el telnet-php que tenemos abierto en el Chrome (navegador del atacante) modificamos la petición con el nuevo identificador que aparece en la nueva sesión del usuario en el Explorer:

Código: php
GET /temas/php-sesion/ejemplos/sesion-registrada/⇒
    autentica2.php HTTP/1.1
Host: localhost
cookie: aseguraSesion=o9o496qi2oo6abedgti31gedb3
Connection: Close


obteniéndose la siguiente respuesta en el telnet-php:

Código: php
HTTP/1.1 302 Found
Date: Sun, 07 Nov 2010 12:02:06 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: aseguraSesion=deleted; expires=Sat, 07-Nov-2009 12:02:05 GMT; path=/
Location: http://localhost/como-se-hace/php-sesion/ejemplos/⇒
    sesion-registrada/acceso-indebido.html
Content-Length: 0
Connection: close
Content-Type: text/html


En este caso el servidor envía una cabecera 302, lo que supone una redirección que se ha ejecutado debido a que hemos incluído un control para evitar secuestro, obligando a redireccionar a la página acceso-indebido.html en lugar de ofrecer la del recurso protegido solicitada autentica2.php. Además se observa que envió un borrado de la cookie de sesión tal como dispusimos en el script para destruir la sesión, que por supuesto no tendrá efecto pues no está en el navegador. En un navegador se redireccionaría a la página acceso-indebido.html, pero en este telnet-php que he escrito no se previsto la posibilidad de redirección. Aunque el propósito está conseguido, verificar que podemos evitar el secuestro de sesión.

Pero, por supuesto, el atacante podría enviar una cabecera User-Agent con el telnet usando la misma cabecera que el usuario (podría probar los User-Agent más frecuentes):

Código: php
GET /temas/php-sesion/ejemplos/sesion-registrada/⇒
    autentica2.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; ...)
Cookie: aseguraSesion=o9o496qi2oo6abedgti31gedb3
Connection: Close


Con el mismo User-Agent que tiene el usuario que se autenticó, el de Internet Explorer que hemos acortado por comodidad, podría llevar a cabo el ataque pues el servidor le devolvería la página protegida:

Código: php
HTTP/1.1 200 OK
Date: Sat, 13 Nov 2010 19:47:06 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 4997
Connection: close
Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    ...
    <p>Ha entrado en este sitio <big><big>PROTEGIDO</big></big> con su nombre
    usuario: <big style="color: green">user</big>.
    ...


El problema aquí es que estamos usando dos cosas para identificar al usuario: el SID de la cookie, que el atacante nos ha robado, y el User-Agent que es un dato que el atacante también puede conocer. Al fin y al cabo cualesquiera otros identificadores que ideemos no dejan de ser otra cosa que sinónimos del SID. Si éste puede robarse, los otros también, pues ambos viajan por el mismo canal HTTP de comunicación a efectos de que el usuario se identifique. Una solución parece que pasa por HTTPS, donde toda la información fluye cifrada.