Underc0de

Programación Web => Back-end => Mensaje iniciado por: Alex en Enero 30, 2013, 10:05:48 PM

Título: Sesiones
Publicado por: Alex en Enero 30, 2013, 10:05:48 PM
Encontré un manual muy interesante de Sessiones de php.

Índice:
    Introducción
   
    Sesiones y cookies
    Sesiones y cookies
    El proceso de una sesión PHP basada en cookies
    Ejemplo de una sesión basada en cookies
    Javascript también puede manejar cookies
    Gestionando una sesión abierta

    Destrucción de sesiones
    Vida de una sesión
    Acceso indebido con sesiones vencidas y no limpiadas
    Destruyendo una sesión
    Destruyendo sesiones inesperadas

    Sesiones expuestas
    Alojamiento compartido en Apache Server
    Un explorador de carpetas con PHP
    Configuración safe_mode en PHP
    Configuración open_basedir en PHP
    Protegiendo las variables de sesión

    Cifrar variables de sesión
    Alojamiento compartido en Apache Server
    Un explorador de carpetas con PHP
    Configuración safe_mode en PHP
    Configuración open_basedir en PHP
    Protegiendo las variables de sesión

    Propagar sesiones
    El identificador de la relación servidor-cliente en una sesión
    Esquema básico para todos los ejemplos
    Propagar SID: Sólo cookies
    Propagar SID: Sólo URL con trans_sid y form=fakeentry
    Propagar SID: Sólo URL con trans_sid y form=action
    Propagar SID: Sólo URL sin trans_sid
    Propagar SID: Cookies o URL con trans_sid
    Propagar SID: Cookies o URL sin trans_sid y usando constante SID
    Propagar SID: POST oculto

    Asegurar sesiones
    Suplantación de sesiones
    Ejemplo de sesión con autenticación
    Fijación de sessión (session fixation)
    Secuestro de sessión (session hijacking)

Fuente (http://www.wextensible.com)
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 10:06:24 PM
Introducción

Cuando decidimos aprender por nuestra cuenta todo lo que podamos sobre el desarrollo web, en algún momento hemos de enfrentarnos con las sesiones. Si el aprendizaje no sigue alguna secuencia temática ordenada, nos encontraremos con problemas que nos obligan a retroceder en ese aprendizaje para retomar cuestiones que habíamos aparcado provisionalmente. Me explico. Quise hacer un sitio web real, como este, pero intentando producir todos los contenidos con el objeto de entenderlos en todos sus entresijos. Y en un momento dado ya tocaba poner un formulario de contacto que remitise los mensajes a un correo electrónico.

Se pone uno manos a la obra y ve que es necesario tener en cuenta el tema de la seguridad con los formularios que remiten datos al servidor. Pues entonces toca estudiarse todo lo que se pueda sobre seguridad. Pero hay algunas técnicas para hacer más seguro un sitio usando sesiones y cookies. Así que no queda más remedio que retroceder un paso y enfrentarse con preguntas como:

    Qué son las sesiones.
    Qué son las cookies.
    Qué relación hay entre ambos conceptos.
    Dónde se almacenan.
    Cómo se crean y destruyen.
    Qué riesgo hay si guardamos datos sensibles en las variables de sesión.
    Cómo se propagan las sesiones.
    Qué es la suplantación de sesión (session hijacking y session fixation).
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 10:07:46 PM
Sesiones y cookies

El término inglés cookie se traduce literalmente por galleta. Fueron introducidas por Lou Montulli en las primeras versiones del navegador Netscape. La razón de su existencia se debe a que el protocolo HTTP no está preparado para el uso de estados. Un estado en informática se define como una configuración de un sistema en un momento dado. Esa configuración viene a ser información que puede almacenarse en algún lugar para uso futuro. Así cuando se cierra una página web no queda ningún rastro de ese proceso. En definitiva las solicitudes HTTP realizadas al servidor son totalmente independientes.

Pero a veces es necesario almacenar ese estado. Por ejemplo, si en nuestro sitio queremos controlar el acceso de usuarios a ciertas páginas, hemos de tener en el servidor una lista de usuarios registrados. Pero en el navegador debe guardarse también la información de registro, pues de otra forma el usuario tiene que identificarse cada vez que pase de una página a otra.

Una cookie es un trozo de texto que un servidor web almacena en el disco duro del ordenador del usuario para recuperarla cuando la necesite. El contenido son parejas de nombre y valor que pueden usarse para almacenar el estado del sistema. Es importante señalar que sólo puede almacenarse texto y con ciertas limitaciones. Por si mismas las cookies no hacen nada, es decir, no son programas ni ninguna otra cosa que puede ejecutar algo no deseado.

En la medida de lo posible hay que buscar los términos en español, pero no podemos traducir cookie directamente por galleta en este contexto. Se puede traducir por su definición como registro de identificación o similar, pero quizás es más apropiado traducirlo como huella tal como hace el navegador Opera. Realmente son huellas que el servidor deja en el navegador para poder identificarlo. De todas formas el término en inglés está tan difundido que quizás es mejor usarlo sin traducir.

Como mucha documentación está en inglés, es conveniente conocer estas definiciones:

State (estado):
    En sistemas que procesan la información, un estado es un conjunto de propiedades transmitidas por un objeto a un observador a través de algún canal. Aplicado a HTTP, las propiedades serán transmitidas por el navegador al servidor, de tal forma que éste determina que información servir en base a las propiedades de ese estado.
Stateless (sin estado), stateful (con estado)
    Son adjetivos que se forman con state less y state full. Significa que un sistema de información trabaja sin o con conocimiento del estado del mismo, respectivamente. HTTP es un sistema stateless (sin manejo de estados), pero con el uso de cookies se convierte en un sistema stateful (con manejo de estados).
Session (sesión)
    Una sesión (de comunicación) es un intercambio no permanente de información entre sistemas. Haciendo HTTP con estados mediante el uso de las cookies, una sesión es el proceso de comunicación entre servidor y navegador para intercambiar la información de estado. Es clásico el ejemplo de una tienda web (el "shopping cart"), donde el usuario selecciona productos que va agregando a su "carrito" a medida que navega entre las páginas del sitio. Las sesiones se caracterizan por:

        Cada sesión tiene un inicio y un final.
        Las sesiones no son permanentes.
        Una sesión puede ser finalizada por el servidor o por el navegador.
        Una sesión siempre está implícita cuando se intercambia información de estado.

    El concepto de sesión es independiente de las cookies, por lo que puede haber sesiones que se manejen sin ellas, tal como veremos más adelante. Sin embargo una cookie está normalmente relacionada con una sesión.

Los documentos que sirven de estándar para implantar HTTP con estados son los siguientes:

    RFC2109 del año 1997 donde se describe inicialmente un protocolo para manejos de estado en HTTP (HTTP State Management Mechanism)
    RFC2965 del año 2000, una revisión y mejora del anterior.

Literalmente el objetivo de estos documentos es especificar una forma para crear un sesión con estado en solicitudes y respuestas con el protocolo HTTP.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 10:11:06 PM
El proceso de una sesión PHP

Tal como señala el manual de PHP sobre sesiones (http://docs.php.net/manual/es/intro.session.php), el concepto es preservar cierta información a través de accesos subsiguientes de los usuarios. El proceso en PHP es el siguiente:

    Un usuario entra por primera vez en una página del sitio que usa sesiones para, a partir de esa página, navegar a otras.
    PHP crea un identificador único para esa sesión (designado para abreviar a veces como ID). Digamos de forma sencilla que es una cadena de números y letras creada por PHP que identifica de forma única un usuario en un momento del tiempo. Esta cadena se genera de forma aleatoria y se almacena en el servidor, estando disponible en el navegador del usuario de dos formas posibles:
        El servidor envía una cookie al navegador del usuario con el identificador. Podemos llamarlas sesiones basadas en cookies.
        El servidor propaga el identificador a través de la URL. En este caso podemos denominarlas sesiones basadas en URL.
    Después de recibir la primera respuesta desde el servidor acompañada del identificador, el navegador hace las siguientes peticiones de otras páginas acompañando el identificador, tanto como cookie o por la URL. Así el servidor identifica al navegador.

Aparte de usar cookies o enviar por la URL el identificador de sesión, podría haber otra forma usando un formulario con un campo de texto oculto. En el tema V sobre propagar sesiones veremos ejemplos de las distintas formas para propagar el identificador de sesión.

Según se desprende del manual PHP, las sesiones basadas en cookies son más seguras que las basadas en URL. Por defecto en el archivo de configuración php.ini (versión 5.2.13) encontramos los siguientes valores para configuraciones de sesiones que pueden ser modificadas en tiempo de ejecución:

La opción session.use_cookies (http://docs.php.net/manual/es/session.configuration.php#ini.session.use-cookies), que especifica si PHP usará cookies para propagar las sesiones. Por defecto se establece en 1 (true), tal como se ve en el php.ini:

; Whether to use cookies.
; http://php.net/session.use-cookies
session.use_cookies = 1


La opción session.use_only_cookies (http://docs.php.net/manual/es/session.configuration.php#ini.session.use-only-cookies), que especifica si PHP sólo usará cookies para almacenar el identificador de sesión en el navegador del cliente, no permitiendo la otra opción de propagarlo por la URL. El valor predeterminado es 1 (true), recomendando usar cookies para evitar la suplantación de sesiones (hijacking), al menos en parte. Esto es lo que aparece en el php.ini:

; This option forces PHP to fetch and use a cookie for storing and maintaining
; the session id. We encourage this operation as it's very helpful in combatting
; session hijacking when not specifying and managing your own session id. It is
; not the end all be all of session hijacking defense, but it's a good start.
; http://php.net/session.use-only-cookies
session.use_only_cookies = 1


La opción session.use_trans_sid (http://docs.php.net/manual/es/session.configuration.php#ini.session.use-trans-sid), que especifica que el identificador sea comunicado a través de URL entre el navegador y el usuario de forma transparente, es decir sin necesidad de incorporar el identificador en cada petición o respuesta. Por defecto es 0 (false). SID es una constante de PHP que contiene el nombre de la sesión y el identificador en la forma "name=ID" o una cadena vacía si se está usando la opción de sesiones basadas en cookies. Para usar sesiones no basadas en cookies sino en URL debemos primero poner la opción session.use_cookies = 0 y luego esta opción a 1. Sin embargo debe tenerse en cuenta el riesgo asumido, tal como se expone en el php.ini:

; trans sid support is disabled by default.
; Use of trans sid may risk your users security.
; Use this option with caution.
; - User may send URL contains active session ID
;   to other person via. email/irc/etc.
; - URL that contains active session ID may be stored
;   in publically accessible computer.
; - User may access your site with the same session ID
;   always using URL stored in browser's history or bookmarks.
; http://php.net/session.use-trans-sid
session.use_trans_sid = 0


Algunas configuraciones pueden modificarse en tiempo de ejecución con la función ini_set(a, b), donde "a" será el string de la variable de configuración y "b" será el nuevo valor a poner. Por ejemplo, para habilitar sesiones basadas en URL haríamos:

ini_set("session.use_cookies", 0);
ini_set("session.use_trans_sid", 1);
session_start();

Traducción del comentario de esta opción:
El uso de trans sid puede poner en riesgo la seguridad de los usuarios. Use esta opción con precaución.

    Un usuario podría enviar una URL conteniendo un identificador de una sesión activa de otro usuario a través de un email, canales IRC, etc.
    Una URL con una sesión activa podría ser almacenada en ordenadores públicamente accesibles.
    Un usuario podría acceder con el mismo identificador de otro usuario utilizando una URL almacenada en el historial del navegador.

Si PHP recomienda usar sesiones basadas en cookies tendremos que hacerle caso, pues ahora mismo no se mucho más del tema. Pero hay que tener en cuenta que en este caso el navegador tiene que aceptar las cookies, pues el usuario puede tenerlas desactivadas. En cuanto a esto, el estándar RFC2965 (http://www.ietf.org/rfc/rfc2965.txt) sobre el mecanismo de estado de sesiones basadas en cookies, en su punto 2 expone literalmente:

"Neither clients nor servers are required to support cookies. A server MAY refuse to provide content to a client that does not return the cookies it sends.": 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, y para empezar, suponemos que el servidor manejará sesiones basadas en cookies que deberán ser aceptadas por el navegador. En el tema V sobre propagar sesiones veremos con ejemplos otras formas de propagar las sesiones para los casos en que las cookies no estuvieran activadas en el navegador.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 10:15:03 PM
Ejemplo de una sesión PHP basada en cookies

Antes de meternos en un ejemplo para ver cómo trabajan una sesión, no debemos olvidar que las sesiones sirven para mantener información entre accesos a distintas páginas. Esa información, que se almacena en el servidor, estará disponible a través de las variables de sesión pero no de forma permanente. Digo esto porque yo mismo pensaba que las sesiones son para eso del "registro y acceso de usuarios", conocido como proceso de autenticación por el cual un sistema verifica que un usuario es quién dice ser, para entonces permitir o denegar el acceso. Las sesiones sirven para esto, pero su principal objetivo es mantener la información entre accesos.

Supongamos que tenemos un catálogo de productos en un sitio. Se compone de un conjunto grande de páginas, cada página para una línea de productos, que a su vez se desglosa en subpáginas con sublíneas. Nos podría interesar ofrecer al usuario un sistema que le ayude a encontrar lo que necesita. Por ejemplo, si en la primera página elige la línea "electrodomésticos" y en la siguiente "frigoríficos", podríamos ofrecerle todos los productos relacionados en esa sublínea y además un conjunto de enlaces relacionados. Algo así como "quizás le interese también congeladores, hornos, lavadoras, ...". Pero si elige "televisores" podríamos sugerirle "cámaras de video, aparatos música, ...".

Ese es un ejemplo de personalización de páginas. Para que un sistema así funcione es necesario almacenar la información previa del camino que va siguiendo el usuario. Para esto pueden servir las sesiones, pero no es estrictamente necesario que el usuario esté registrado en alguna base de datos.

Usaremos un ejemplo más simple. Sea una primera página (pagina1.php) que abre una sesión para navegar por un conjunto siguiente de páginas (pagina2.php y otras). Supongamos que deseamos abrir la sesión con objeto de personalizar el conjunto de páginas segunda y siguientes. Para simplificar el ejemplo hacemos que la personalización sea sólo para el color del fondo de las páginas. Podemos ya entrar en la pagina1.php que tiene el siguiente código (obviamos con puntos suspensivos el HTML que no es de interés):

<?php
//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 1);
session_start();
//Por defecto asignamos un color para el fondo. Si esta es la
//primera vez que abrimos la sesión, le pone el color amarillo.
if (!isset($_SESSION['color-fondo'])) {
    //Esto evita la fijación de sesión
    session_regenerate_id(true);
    //Ahora asignamos el parámetro de sesión por primera vez
    $_SESSION['color-fondo']  = 'yellow';
}
//Ahora se construye la página
?>

<!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">
    ...
    <h3>Página 1</h3>
    <p>Esta página ... el color del fondo, que ahora tiene el valor
    <code><?php echo htmlspecialchars($_SESSION['color-fondo']); ?></code>,
    color de fondo que se pondrá en las páginas 2 y 3.
    </p>
    <p>Podemos ver la cookie que almacenó PHP en el navegador:<br />
    <code id="mi-cookie"></code>
    </p>
    <script>
        var arrayCookies = document.cookie.split(";");
        for (var i=0; i<arrayCookies.length; i++){
            var unaCookie = arrayCookies.split("=");
            var nombre = quitaEspacios(unaCookie[0]);
            if (nombre == "PHPSESSID") {
               var contenido = escapaHtml(arrayCookies);
               setInnerText(document.getElementById("mi-cookie"), contenido);
               break;
            }
        }
    </script>   
    ...

   

En color marrón está el código PHP y en azul el HTML, incluso el de JavaScript. Los comentarios están en verde. En primer lugar vemos que para iniciar una página con sesión hemos de reconfigurar las opciones de inicio session.use_cookies y session.use_only_cookies para que la sesión sólo use cookies. Es una medida extra de seguridad por si la configuración se hubiera modificado, pues en otro caso se usaría URL para enviar la sesión y esta es una vía que no se aconseja como vimos más arriba. Luego hacemos session_start() antes que se envíe cualquier cosa al navegador. Con esto se inicia la sesión pero no la consideramos activa hasta que guardemos algún parámetro de la sesión como $_SESSION['color-fondo'] = 'yellow'. Para saber si ya la sesión fue iniciada antes miramos con isset() si nuestro parámetro color-fondo está ya registrado. Si no lo estuviera es que es el primer inicio de la sesión y lo registramos con un color de fondo amarillo, por ejemplo.

Entonces el servidor creará un identificador único para esa sesión. Este identificador se mantiene mientras mantengamos abierto el navegador, por lo que cada vez que accedamos a las páginas del ejemplo veremos que se mantiene el color de fondo seleccionado. Este ejemplo se ejecuta en mi servidor Apache + PHP montado en local sólo para aprender. Puede ver más detalles de la instalación en Apache 2.2.15 y PHP 5.2.13 en Windows. Buscando el php.ini encontramos una línea con session.save_path="C:\WINDOWS\Temp", que es el lugar donde se configura para guardar las sesiones. Esta es una carpeta que usa el servidor para almacenar la sesión, cuyo contenido después de ejecutar la primera página es el siguiente:

(http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion/temp-servidor.jpg)

Crea un archivo con el nombre iniciado con sess_q75n4oq6turdv67uohqkbl7lv5. La ristra después de sess_ es el identificador único de esa sesión. Si abrimos el archivo con un bloc de notas podemos ver que contiene color-fondo|s:6:"yellow";. Se trata del parámetro que le pasamos con el color de fondo. Realmente no tenemos que preocuparnos por esto pues nos basta con crear nuevos parámetros para una sesión simplemente haciendo $_SESSION["parametro"] = $algun_valor. Para recuperar un parámetro bastaría hacer $variable = $_SESSION["parametro"]. (La barra vertical separa el nombre del valor del parámetro y que la s:6: significa que el valor es un string con 6 caracteres).

A continuación el servidor crea el documento pagina1.php que será enviado al navegador del usuario. Lo que se envía incluyendo la cabecera HTTP será algo como esto:

HTTP/1.1 200 OK
Date: Sun, 12 Sep 2010 22:46:25 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Set-Cookie: PHPSESSID=q75n4oq6turdv67uohqkbl7lv5; path=/
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: 2031
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>Página 1</title>
    ... continúa el resto de la página ...


Esto lo he obtenido mediante un script PHP que hace algo parecido al Telnet en Windows. Puede ver más detalles en telnet con php. En todo caso lo que nos interesa apreciar es que el servidor envia una orden para que el navegador albergue una cookie. Ésta contiene el identificador de la sesión, el mismo que almacena en la carpeta de sesiones C:/Windows/Temp como vimos antes. El nombre dado a la cookie siempre es PHPSESSID. Este nombre se configura en el php.ini inicialmente por defecto (aunque puede modificarse):

; Name of the session (used as cookie name).
; http://php.net/session.name
session.name = PHPSESSID   

   

Un detalle interesante es la fecha de expiración de la cookie. Se observa una fecha anterior, el 19/11/1981, con lo que la cookie que se almacena en el navegador tiene una duración de vida mientras el navegador esté abierto. Por lo tanto sólo va a existir en memoria y no será almacenada en el disco duro. En ese caso y con Windows se grabarían en las carpetas de documentos y configuración, en algún lugar de la carpeta del usuario. Pero creo que no viene al caso saber donde se almacenan, pues todos los navegadores nos dan la posibilidad de consultarlas y, en algún caso, hasta modificarlas.

Podemos ver las cookies en Internet Explorer 8 mediante herramientas de desarrollo, luego en caché y en ver información de cookies. Este navegador construye una página para mostrar todas las cookies almacenadas, por lo que podemos buscar al final del documento, apareciendo algo como esta imagen, donde el identificador es igual que el primero de la lista de la imagen de más arriba:

(http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion/cookie-ie.jpg)

Es importante ver que Internet Explorer no muestra el dominio del que procede cuando estamos trabajando con localhost, pero que debe ser algún error puesto que para el resto de cookies si se ve el dominio. De todas formas aprovechamos para decir que las cookies de un dominio sólo pueden ser gestionadas por ese dominio, por lo que es un dato de suma importancia que se almacena con la cookie. Observe el tiempo de expiración at the end of the session, al finalizar la sesión. En los siguientes navegadores veremos que se aporta más información.

En Opera 10.61 podemos verlas en herramientas, avanzados, huellas(cookies) con algo como esto. Este navegador es el que me parece más completo al mostrar las cookies (huellas como le llama), pues ofrece además la posibilidad de editarlas. También se puede acceder con el botón derecho del ratón con la opción editar opciones del sitio, huellas, mostrando todas las cookies del dominio de esa página. Se corresponde con el segundo identificador de la lista mencionada.

(http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion/cookie-op.jpg)

En Firefox 3.6.8 podemos verlas en opciones, privacidad, mostrar cookies con algo como esto, por supuesto con otro identificador de sesión. También podemos verlas con el botón derecho del ratón en la página, con la opción ver información de la página y luego en seguridad, obteniéndose todas las cookies para el dominio de esa página. El identificador es el mismo que el tercero de la lista del /Temp en el servidor:

(http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion/cookie-ff.jpg)

En Safari 4.0.5 están las cookies en el botón de ajustes generales, en la opción de preferencias, seguridad. El identificador es el mismo que el último de la lista en el /Temp del servidor:

(http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion/cookie-sa.jpg)

En cualquier caso la cookie con el identificador de la sesión se almacena en el navegador del usuario. A partir de aquí y siempre dentro de la duración de la sesión, el navegador acompañará esta cookie con las solicitudes de páginas que se hagan a ese mismo servidor. Éste comprueba que el identificador es igual que el que tiene almacenado en la carpeta de sesiones del servidor (en C:/Windows/Temp para mi servidor local), en cuyo caso pone a disposición de uso los parámetros allí almacenados (el color de fondo para nuestro ejemplo).

La importancia de guardar sólo un identificador en el navegador del usuario radica en que es un dato sin valor en sí mismo. Veámos, ¿porqué no guardamos también los parámetros como el color de nuestro ejemplo en la cookie?. Existe la función setcookie() en PHP que permite enviar cookies desde el servidor al navegador. Mientras que con la variable superglobal $_COOKIE podemos verla en el servidor. Podemos argumentar que así no estamos almacenando nada en el servidor. ¿Pero si en lugar de ser un simple color para el fondo de las páginas fuera un dato personal que consideramos privado?. Dónde es más seguro tenerlo, ¿en el servidor o en el navegador?. Por ahora respondemos que en el servidor, siguiendo el criterio general de PHP que es almacenar el identificador en el navegador y el resto de la información en el servidor. Espero que a medida que desarrolle estos temas pueda dar más luz sobre este asunto.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 10:15:52 PM
Javascript también puede manejar cookies

En el ejemplo del código de la pagina1.php anterior teníamos un trozo de código en Javascript que reproducimos otra vez:

    ...
    <code id="mi-cookie"></code>
    </p>
    <script>
        var arrayCookies = document.cookie.split(";");
        for (var i=0; i<arrayCookies.length; i++){
            var unaCookie = arrayCookies[i].split("=");
            var nombre = quitaEspacios(unaCookie[0]);
            if (nombre == "PHPSESSID") {
               var contenido = escapaHtml(arrayCookies[i]);
               setInnerText(document.getElementById("mi-cookie"), contenido);
               break;
            }
        }
    </script> 
    ... 

   

Podemos ver que usamos document.cookie que nos devuelve un string con todas las cookies del dominio de esa página almacenadas en el navegador. La colección de cookies de un dominio son parejas nombre=valor separadas por punto y coma (;). Así haciendo un split(";") sobre ese string obtenemos un array de parejas nombre=valor, que luego separamos otra vez para obtener el nombre y buscar el que nos interesa, PHPSESSSID para mostrar el identificador en la página de ejemplo. Así veremos que realmente el identificador se almacenó en una cookie de nuestro navegador.

Las tres funciones señaladas quitaEspacios(), escapaHtml() y setInnerText() las he creado para anular espacios antes y después de una cadena, escapar caracteres reservados e insertar texto dentro de un elemento.

No quiero extenderme ahora mucho sobre el manejo de cookies con JavaScript, pues mi interés está ahora en las que se gestionan desde un servidor para el uso de sesiones basadas en cookies. La única razón de exponerlo es para corroborar que esa cookie de sesión está realmente alojada en nuestro navegador.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 10:17:13 PM
Gestionando una sesión abierta

A partir de la página 1 del ejemplo podemos acceder a las siguientes páginas, pagina2.php y pagina3.php, ambas de igual estructura. Una vez que pasemos por la página 1 y mientras no cerremos el navegador, la cookie seguirá almacenada en el navegador, por lo que aunque cerremos esa página 1 y mientras el navegador esté abierto, la sesión seguirá viva. Bueno a excepción de que no se destruya si acaba su duración de vida como veremos posteriormente. El código de las páginas 2 y 3 es el siguiente (son iguales), donde ponemos puntos suspensivos en el HTML que no es de interés:

Código (php) [Seleccionar]
<?php
//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
ini_set("session.use_cookies"1);
ini_set("session.use_only_cookies"1);
session_start();
//Establecemos una variable para el color del fondo por defecto
$un_color_fondo "white";
//Comprobamos si la sesión ya fue iniciada
if (!isset($_SESSION["color-fondo"])){
    
//Para evitar la fijación de sesión regeneramos el id
    //cuando la sesión no haya sido iniciada por el cauce previsto
    
session_regenerate_id(true);
    
//Con sesiones no iniciadas ponemos un color rojo
    
$un_color_fondo "red";
} else {
    
//Aunque un parámetro de sesión proviene de una carpeta del
    //servidor, no está de más filtrar caracteres no deseados,
    //pues este color se insertará en el style del body.
    
$un_color_fondo htmlspecialchars($_SESSION["color-fondo"], ENT_QUOTES);
    
//Si está página se está recibiendo por la ejecución del botón
    //submit del formulario, tomamos el color seleccionado
    
if (isset($_POST["color"])){
        
//Filtramos caracteres no deseados
        
$un_color_fondo htmlspecialchars($_POST["color"], ENT_QUOTES);
        
//Eliminamos comillas por si el servidor tiene activado el
        //escape con barras invertidas
        
if (get_magic_quotes_gpc() == 1$un_color_fondo stripslashes($un_color_fondo);
        
//Por último actualizamos el parámetro de la sesión con el nuevo color
        
$_SESSION["color-fondo"] = $un_color_fondo;
    }    
}
?>


<!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">
...
<!-- Insertamos en el body el color de fondo -->
<body style="background-color:<?php echo $un_color_fondo?>;" >
    ...   
    <h3>Página 2</h3>
    <p>Esta página2 tiene el valor
    <big><big><?php echo $un_color_fondo;?></big></big>
    para la propiedad <code>background-color</code> del cuerpo del documento.
    </p>
    <?php if($un_color_fondo == "red"){?>
        <p style="font-size: 2em;">Ha llegado a esta página sin antes haber pasado
        por la página 1.</p>
    <?php } else {?>
        <!-- Este formulario sirve para que el usuario seleccione otro color para
        el fondo. Al enviarlo se recoge en esta misma página, pues en el inicio
        hemos puesto el script para ello. -->
        <form action="pagina2.php" method="post">
            <label>Seleccione color para el fondo:
                <select name="color">
                    <option value="yellow"
                    <?php if ($un_color_fondo == "yellow") echo "selected='selected'" ?>
                    >amarillo (yellow)</option>
                    <option value="aqua"
                    <?php if ($un_color_fondo == "aqua") echo "selected='selected'" ?>
                    >azul (aqua)</option>
                    <option value="lime"
                    <?php if ($un_color_fondo == "lime") echo "selected='selected'" ?>
                    >verde (lime)</option>
                </select>
            </label>
            <input type="submit" value="enviar" />
        </form>       
    <?php ?>
    ...(aquí va el JavaScript como el de pagina1.php)...


Recordamos que el proceso para este ejemplo es que el usuario entrase a través de la primera página (pagina1.php), de tal forma que allí se creaba una nueva sesión guardando por primera vez el parámetro $_SESSION['color-fondo'] = 'yellow' si la sesión no hubiese estado iniciada. Recordamos que una sesión se mantiene abierta mientras no se cierre el navegador. Al pasar a la segunda página con el enlace dispuesto en la primera página, lo primero que hacemos es iniciar sesión otra vez con session_start(). Luego hacemos una comprobación de seguridad para ver si el parámetro existe con !isset($_SESSION["color-fondo"]). Lo que se espera es que exista, pues habiendo estado antes (en algún momento) en la primera página tiene que haber sido creado ya el color de fondo. ¿Qué razones hay para que no exista?. Una razón podría ser intentar acceder a pagina2.php sin antes haber estado en la primera página.

Las sesiones no son permanentes, por lo que tienen un inicio y un final. Una forma de finalizar una sesión es cerrar el navegador, pero con esta opción no podríamos acceder directamente a la página 2 con enlace desde la 1, aunque si podríamos acceder poniendo en la barra de direcciones del navegador la URL completa de la página 2. Si cerramos el navegador para que no haya sesión abierta y luego lo abrimos poniendo la URL de pagina2.php en la barra de direcciones del navegador, comprobaremos que el script PHP detecta que no existe la sesión, es decir, el parámetro color de fondo para esa sesión usando if (!isset($_SESSION["color-fondo"])). Si es así le ponemos el color de fondo rojo. Además regeneramos el identificador con session_regenerate_id(). Con esto tratamos de impedir las fijaciones de sesión, aspecto que trataré de explicar más adelante (si es que al final logro entenderlo).

Otra forma de acceder a pagina2.php sin usar la entrada de la página primera podría ser con el telnet con php que comenté más arriba. Con esta cadena de petición:

GET /temas/php-sesion/ejemplos/proceso-sesion/pagina2.php HTTP/1.1
Host: localhost
cookie: PHPSESSID=falso-id
Connection: Close

Poniendo cualquier identificador de sesión como el señalado en azul, el servidor nos responderá devolviendo la página 2:

Código (php) [Seleccionar]
HTTP/1.1 200 OK
Date: Thu, 16 Sep 2010 09:41:45 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: PHPSESSID=nlikqad9ib1563tgod36ggfa84; path=/
...
<!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">
...
<body style="background-color:red;" >
    ...
    <p>Esta página2 tiene el valor <big><big>red</big></big>
    para la propiedad <code>background-color</code> del cuerpo del documento.</p>
    <p style="font-size: 2em;">Ha llegado a esta página sin antes haber pasado
    por la página 1.</p>
    ...


En este caso al no existir una sesión anterior con ese identificador, tampoco habrá un parámetro de color guardado en $_SESSION. Entonces se regenera el identificador tal como podemos ver que se pasa en la cabecera de la respuesta y se pone el color rojo para el fondo de la página. El identificador falso-id que se pasó es guardado en la carpeta Temp con el resto de sesiones, pero al regenerar la sesión con session_regenerate_id(true) usando el argumento true lo que hacemos es eliminar ese archivo de falsa sesión.

Otro aspecto de las sesiones es el tiempo de vida que poseen. Esto también es una razón por la cual deberíamos inspeccionar en pagina2.php si existen las variables de sesión (el parámetro de color en nuestro ejemplo). Las sesiones que ya no se utilizan son eliminadas del sistema, tanto en memoria como en disco. Esto lo veremos en el próximo tema que habla sobre la destrucción de sesiones.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 11:52:12 PM
Destrucción de sesiones

Vida de una sesión

En el archivo de configuración php.ini de mi servidor local podemos ver esta entrada:

; After this number of seconds, stored data will be seen as 'garbage' and
; cleaned up by the garbage collection process.
; http://php.net/session.gc-maxlifetime
session.gc_maxlifetime = 1440


Recuerde que todo lo que aparece aquí ha sido probado en mi servidor local Apache+PHP montado para el aprendizaje, pues no conviene exponer las configuración del servidor real donde está viendo estas páginas por una cuestión de seguridad.

La máxima duración de una sesión se establece por defecto en 1440 segundos (24 minutos). La traducción del comentario dice que "Después de este número de segundos, los datos almacenados serán vistos como 'basura' y limpiados por el proceso de recolección de basura." La recolección de basura sucede durante el inicio de sesiones pero no siempre, pues depende de las siguientes configuraciones del php.ini:

; Defines the probability that the 'garbage collection' process is started
; on every session initialization. The probability is calculated by using
; gc_probability/gc_divisor. Where session.gc_probability is the numerator
; and gc_divisor is the denominator in the equation. Setting this value to 1
; when the session.gc_divisor value is 100 will give you approximately a 1% chance
; the gc will run on any give request.
; Default Value: 1
; Development Value: 1
; Production Value: 1
; http://php.net/session.gc-probability
session.gc_probability = 1

; Defines the probability that the 'garbage collection' process is started on every
; session initialization. The probability is calculated by using the following equation:
; gc_probability/gc_divisor. Where session.gc_probability is the numerator and
; session.gc_divisor is the denominator in the equation. Setting this value to 1
; when the session.gc_divisor value is 100 will give you approximately a 1% chance
; the gc will run on any give request. Increasing this value to 1000 will give you
; a 0.1% chance the gc will run on any give request. For high volume production servers,
; this is a more efficient approach.
; Default Value: 100
; Development Value: 1000
; Production Value: 1000
; http://php.net/session.gc-divisor
session.gc_divisor = 100   

   

CitarAlgunas configuraciones pueden modificarse en tiempo de ejecución con la función ini_set(a, b), donde "a" será el string de la variable de configuración y "b" será el nuevo valor en segundos a poner. Estas tres variables session.gc_maxlifetime, session.gc_probability y session.gc_divisor pueden modificarse en tiempo de ejecución.

En definitiva, una sesión tiene una duración de 24 minutos. Pasado este tiempo si no se ha usado, entonces pasará a ser basura. Pero el recolector la limpiará de la carpeta Temp sólo cuando se inicien nuevas sesiones pero no en todos los inicios. La probabilidad de que se inicie el recolector será la división entre session.gc_probability y session.gc_divisor. Tal como está por defecto esta probabilidad es del 1%, aunque en sistemas en producción (en un servidor real) es conveniente reducirla poniendo el valor 1000 para session.gc_divisor.

Para probar que esto funciona, durante las pruebas con las sesiones del ejemplo anterior comprobé en la carpeta Temp de mi servidor local que habían muchas sesiones registradas. Pero también observé que sólo estaban las más recientes, pues las de días anteriores habían desaparecido. El recolector de basura había estado limpiándolas. Entonces modifique el session.gc_probability con un valor 100, así que la probabilidad de limpieza con la apertura de una sesión debería ser del 100%. También modifiqué el session.gc_maxlifetime a 60 segundos para comprobar la duración de vida. Luego restauré el Apache y abrí la pagina1.php del ejemplo. Y, efectivamente, eliminó todas las sesiones del Temp antes de crear la nueva sesión. Luego esperé 1 minuto y cerré el navegador y abrí pagina1.php otra vez para comprobar que elimina esa sesión anterior pues ya se había vencido su vida, lo cual hizo antes de grabar la nueva sesión.

Por supuesto, es de entender que una probabilidad del 100% significa que el servidor estará continuamente ejecutando esa acción con cada apertura de sesión. Si el servidor tiene muchos usuarios abriendo sesiones, el sistema se ve muy forzado. Por eso se recomienda una probabilidad de 1/1000 (0.1%) en un caso real, aunque por defecto tiene 1/100 (1%).
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 11:52:59 PM
Acceso indebido con sesiones vencidas y no limpiadas

Si antes de vencer el período de vida establecido se abre una página que llama a una sesión existente en el Temp, lo que se hace con sesion_start(), entonces el contador de tiempo vuelve a iniciarse para esa sesión. Pero también he comprobado que si una sesión tiene el período vencido pero aún no ha sido limpiado por el recolector de basura y se activa de nuevo con sesion_start(), también en este caso el contador vuelve a iniciarse.

Si las sesiones con el identificador y las variables de sesión se guardan en el servidor, parecería que no tendríamos porque preocuparnos. Si hay sesiones que no se están usando, vencidas o no, en la carpeta Temp ¿qué podría pasar?. Uno de los problemas sobre el que he leído es que muchas veces los sitios web se alojan en servidores compartidos, donde pudiera ser que todas las sesiones de todos los sitios de una máquina se alojaran en una misma carpeta Temp. ¿Podría otra persona acceder a las sesiones de mi sitio?. No tengo una respuesta para esta pregunta, pero sería recomendable no dejar sesiones sin usar en ningún lado.

Usando mi servidor local (no este real donde está viendo estas páginas) he ido a la carpeta Temp y he buscado la última sesion que he usado en los ejemplos de pagina1.php y siguientes. Esta sesión estaba vencida después de pasar más de 24 minutos pero dado que no había abierto ninguna sesión nueva, el recolector de basura ni siquiera tenía alguna probabilidad de limpiarla. Comprobado que contenía el color de fondo color-fondo|s:5:"green";, he abierto mi telnet con php para lanzar una petición al servidor usando pagina2.php con una cookie igual al identificador de sesión:

GET /temas/php-sesion/ejemplos/proceso-sesion/pagina2.php HTTP/1.1
Host: localhost
cookie: PHPSESSID=l2i5h7s7rdeooe74ecfqm64u42
Connection: Close


Y esta es la repuesta desde el servidor, donde hemos obviado alguna líneas no interesantes para el tema:

HTTP/1.1 200 OK
Date: Thu, 16 Sep 2010 17:52:00 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: 2955
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>Página 2</title>
    ...
<!-- Insertamos en el body el color de fondo -->
<body style="background-color:green;" >
    ...


Se observa que se pudo reabrir la sesión vencida e incluso mostrarse el fondo de color verde. ¿Cómo podemos evitar esto?, es decir ¿cómo podemos evitar que se reabra una sesión sin que se usen los cauces previstos para ello?. El "cauce" previsto en nuestro ejemplo era ir por la pagina1.php. Si hemos cerrado y abierto el navegador, indepedientemente de que hayan sesiones vencidas o no en Temp, si vamos a pagina1.php el servidor construirá una nueva sesión. Esta debería ser la forma correcta para poder controlar las sesiones. Entonces el asunto estaría en destruir las sesiones que ya no necesitemos y no esperar por el "camión de la basura".
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 11:53:49 PM
Destruyendo una sesión

Para destruir una sesión parece que podemos usar la función session_destroy(). Según dice el manual PHP, esta función destruye toda la información asociada con la sesión actual. No destruye ninguna de las variables globales asociadas con la sesión, ni destruye la cookie de sesión. Las variables pueden aún volver a usarse si volvemos a llamar a session_start(). En otro caso, si se desean eliminar, podemos iniciar el array con $_SESSION = array();. En cuanto a eliminar la cookie del usuario, debe usarse setcookie().

Pongámonos en marcha para comprobar esto. Antes de probar este ejemplo, primero tendríamos que tener abierta una sesión, lo cual puede hacerse llamando al ejemplo anterior pagina1.php desde aquí mismo, en caso en que en el tema anterior no hubiese ya entrado en ese ejemplo, donde podemos ir a la pagina3.php con un vínculo que nos lleva a la página destruye-sesion.php. El código importante de este ejemplo es:

<?php
//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
ini_set("session.use_cookies"1);
ini_set("session.use_only_cookies"1);
session_start();
//Primero destruimos las variables de sesión, en nuestro ejemplo "color-fondo"
$_SESSION = array();
//A continuación envíamos una cookie con tiempo negativo, lo que supone que
//el navegador eliminará la cookie en su sistema.
//Esto lo podemos hacer sólo si el php.ini está configurado para usar
//cookies, aunque por defecto es que sí.
if (ini_get("session.use_cookies")) {
    
$params session_get_cookie_params();
    
setcookie(session_name(), ''time() - 42000,
        
$params["path"], $params["domain"],
        
$params["secure"], $params["httponly"]
    );
}
//Por último destruimos la sesión
session_destroy();
?>


<!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>
    ...


El HTML de esta página no tiene ningún contenido creado con PHP, aunque si tiene el mismo JavaScript que las de los otros ejemplos. Por eso obviamos el código pues no tiene mayor interés.

Se puede comprobar que destruye también el archivo de sesión de la carpeta Temp e incluso la cookie del navegador. Si ahora volvemos a pagina2.php o pagina3.php veremos que aparece con fondo rojo, indicativo de que no existe sesión abierta. Bueno parece que no queda ningún rastro y que esto es lo que estaba buscando. Pero queda un tema pendiente, ¿que pasa con la personalización del color del fondo?. Por lo visto no podemos usar sesiones para guardar particularidades de usuarios y, al mismo tiempo, querer destruirlas para evitar que alguién ajeno acceda a ellas.

Podríamos sólo destruir el archivo en Temp y dejar las variables de sesión vivas en memoria, $_SESSION["color-fondo"] en nuestro caso. Eso lo hacemos con el ejemplo destruye-parcial-sesion.php, cuyo vínculo también puede ver en la pagina3.php del ejemplo. El código es igual que el anterior, pero quitamos $_SESSION = array() y agregamos lo siguiente:

...
$un_color_fondo = "white";
if (isset($_SESSION["color-fondo"])){
    $un_color_fondo = htmlspecialchars($_SESSION["color-fondo"], ENT_QUOTES);
}
?>
...
<!-- Insertamos en el body el color de fondo -->
<body style="background-color:<?php echo $un_color_fondo?>;" >
...


Esto evidencia que la variable de sesión $_SESSION["color-fondo"] sigue estando en memoria. Este color lo aplicamos al cuerpo de la página. Lo único es que las variables de sesión sólo nos servirían para esta página, pues si volvemos a las páginas 2 o 3 veremos que aparecerán con el fondo rojo debido a que no hay sesión previamente abierta.

Pero no tiene ningún sentido destruir una sesión cuando lo que estamos buscando es precisamente poder usar sesiones, aunque tenemos que lograr que sean sesiones esperadas. En otro caso si podemos destruirlas.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 11:54:37 PM
Destruyendo sesiones inesperadas

En los ejemplos anteriores veíamos que se ponía el fondo de color rojo cuando se llegaba a las pagina2.php o pagina3.php sin antes haber pasado por pagina1.php, es decir, sin que existiera previamente una sesión abierta. Podemos modificar las páginas para destruir la sesión y redirigir al usuario a la pagina1.php. En este ejemplo repetimos esas 3 páginas y las renombramos en otra carpeta como pagina1b.php, pagina2b.php y pagina3b.php. El procedimiento será el mismo: entrando por la primera página abrimos sesión. Desde esa página tenemos vínculos a las siguientes manteniendo la sesión, pero si entramos por las siguientes sin sesión abierta previa destruiremos la sesión y nos redigirá a la primera página.

El ejemplo está disponible en este enlace a pagina1b.php

Se comprueba que funciona si copiamos la URL de la pagina2b.php e intentamos acceder directamente a ella. Por ejemplo, cerrando el navegador y abriéndolo de nuevo, ponemos en la barra de direcciones del navegador la ruta http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion-b/pagina2b.php para intentar acceder a la segunda página. Veremos que se abrirá la primera página.

También puede probarlo si, arrancando desde el navegador cerrado en caso de haber estado antes en pagina1b.php, usa este enlace que lleva a pagina2b.php o al de pagina3b.php, enlaces que redigirán a la primera página si no existe sesión previa.

La pagina1b.php tiene casi el mismo código que pagina1.php del ejemplo anterior, aunque sólo se modifica en esto:

//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
//Además le damos un nombre particular a esta sesión.
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 1);
session_name("sesionB");
session_start();
...

    ...
    <script>
        var arrayCookies = document.cookie.split(";");
        for (var i=0; i<arrayCookies.length; i++){
            ...
            if (nombre == "sesionB") {
                ...
            }
        }
    </script> 
    ...


Hacemos notar lo resaltado en amarillo session_name("sesionB"), sentencia que debe ir antes de session_start(). En el tema anterior decíamos que el nombre de la sesión era PHPSESSID si no se especificaba otra cosa. Pero podemos manejar más de una sesión para un mismo navegador. Si previamente se usó el ejemplo del conjunto de páginas pagina1.php y siguientes, en caso de que aún no se hubiese cerrado el navegador esa sesión PHPSESSID estaría "viva". Eso quiere decir que su variable de sesión $_SESSION["color-fondo"] seguirá existiendo, por lo que si llamamos a este nuevo conjunto de páginas pagina1b.php y siguientes, tomará esa variable de sesión si tiene el mismo nombre. Por lo tanto si deseamos mantener este segundo conjunto de páginas protegidas en un entorno de variables independientes (aunque se llamen igual), hemos de darle un nombre de sesión distinto, tal como hemos hecho con session_name("sesionB"). La cookie del navegador también hemos de buscarla por este nombre nuevo de sesión.

Las páginas 2 y 3 sólo son modificadas en el script PHP, pues lo demás es igual. Vemos la pagina2b.php

<?php
//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo con cookies por si estuviera desactivado
//Además le damos un nombre particular a esta sesión.
ini_set("session.use_cookies"1);
ini_set("session.use_only_cookies"1);
session_name("sesionB");
session_start();
//Establecemos una variable para el color del fondo por defecto
$un_color_fondo "white";
//Comprobamos si la sesión ya fue iniciada
if (!isset($_SESSION["color-fondo"])){
    
//Si se ha llegado aquí es que es una sesión inesperada.
    //Primero destruimos las variables de sesión
    
$_SESSION = array();
    
//A continuación eliminamos la cookie del navegador
    
if (ini_get("session.use_cookies")) {
        
$params session_get_cookie_params();
        
setcookie(session_name(), ''time() - 42000,
            
$params["path"], $params["domain"],
            
$params["secure"], $params["httponly"]
        );
    }
    
//Por último destruimos la sesión
    
session_destroy();
    
//Rederigimos al usuario a pagina1b.php
    
$host  $_SERVER["HTTP_HOST"];
    
$uri   rtrim(dirname($_SERVER["PHP_SELF"]), "/\\");
    
$extra "pagina1b.php";
    
header("Location: http://$host$uri/$extra");
    
//Tras la redirección e inmediatamente salimos del script
    
exit;
} else {
    
//Aunque un parámetro de sesión proviene de una carpeta del
    //servidor, no está de más filtrar caracteres no deseados,
    //pues este color se insertará en el style del body.
    
$un_color_fondo htmlspecialchars($_SESSION["color-fondo-b"], ENT_QUOTES);
    
//Si está página se está recibiendo por la ejecución del botón
    //submit del formulario, tomamos el color seleccionado
    
if (isset($_POST["color"])){
        
//Filtramos caracteres no deseados
        
$un_color_fondo htmlspecialchars($_POST["color"], ENT_QUOTES);
        
//Eliminamos comillas por si el servidor tiene activado el
        //escape con barras invertidas
        
if (get_magic_quotes_gpc() == 1$un_color_fondo stripslashes($un_color_fondo);
        
//Por último actualizamos el parámetro de la sesión con el nuevo color
        
$_SESSION["color-fondo-b"] = $un_color_fondo;
    }    
}
?>


<!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">
    ...


Para redirigir al usuario usamos la función header(). Nos fijamos en un ejemplo que hay en ese manual PHP, en relación con una nota que dice que HTTP/1.1 requiere una URI absoluta como argumento a Location. Expone un ejemplo que hemos trasladado. Por un lado la variable global $_SERVER nos permite acceder a las variables de entorno del servidor. Así $_SERVER["HTTP_HOST"] contiene el dominio de la página actual. Luego $_SERVER["PHP_SELF"] contiene la ruta del actual script que se está ejecutando. En el ejemplo la ruta completa del script es la que señalamos más arriba http://www.wextensible.com/temas/php-sesion/ejemplos/proceso-sesion-b/pagina2b.php por lo que $_SERVER["PHP_SELF"] contiene la ruta relativa al raíz, es decir, temas/php-sesion/ejemplos/proceso-sesion-b/pagina2b.php.

Obtenemos la carpeta con dirname(). Por último con la función rtrim(ruta, "/\\") quitamos los espacios a la derecha de una ruta y también la última barra de la derecha. Ponemos las dos barras "/\" (aunque escapamos la segunda por estar dentro de un string), pues en Windows se pueden usar ambas y hemos de usar siempre las barras derecha, porque luego concatenemos todo con barras derecha. Inmediatamente después salimos del script con exit, con lo que no enviamos el HTML de la página 2 sino que redirigimos a la página 1.

Aunque hemos avanzado algo, esto no excluye la posibilidad de "robar" una sesión de la carpeta Temp y, mediante una aplicación como telnet con php, acceder a pagina2b.php sin que el servidor note la diferencia. En el próximo tema sobre sesiones expuestas intentaré buscar una solución.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 11:56:50 PM
Sesiones expuestas

Alojamiento compartido en Apache Server

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

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

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

Include conf/extra/httpd-vhosts.conf
...


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

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

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

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

127.0.0.1       localhost   

   

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

...
127.0.0.1       localhost
127.0.0.1       localhost1
127.0.0.1       localhost2   

   

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

NameVirtualHost *:80

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

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

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


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

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

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

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

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

Vemos en un navegador el dominio localhost:

(http://www.wextensible.com/temas/php-sesion/ejemplos/vhost/localhost.gif)

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

(http://www.wextensible.com/temas/php-sesion/ejemplos/vhost/localhost1.gif)

así como localhost2

(http://www.wextensible.com/temas/php-sesion/ejemplos/vhost/localhost2.gif)

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

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

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


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

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


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

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


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

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

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

(http://www.wextensible.com/temas/php-sesion/ejemplos/vhost/explorador.gif)

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

En esta aplicación he agregado la lectura dos opciones de configuración de PHP que restringuen el acceso a carpetas. Son safe_mode y open_basedir que analizaremos a continuación. Además se leen los uid y gid, identificadores de usuario y grupo que también analizaremos.
Título: Re:Sesiones
Publicado por: Alex en Enero 30, 2013, 11:58:46 PM
Configuración safe_mode en PHP

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

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

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


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

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

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

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


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

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


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

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


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

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

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

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

    Cuándo se puede cambiar una configuración
    Listado configuraciones PHP
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:00:35 AM
Configuración open_basedir de PHP

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

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


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

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

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

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

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

   

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

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

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

(http://www.wextensible.com/temas/php-sesion/ejemplos/vhost/sin-open-basedir.gif)

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

(http://www.wextensible.com/temas/php-sesion/ejemplos/vhost/con-open-basedir.gif)

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

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

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

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

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

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

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


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

(http://www.wextensible.com/temas/php-sesion/ejemplos/vhost/open-basedir-sitio1.gif)

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

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

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

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

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

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

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

En el tema anterior decíamos que con el uso de sesiones en alojamientos compartidos alguién podría acceder a las variables de sesión en la carpeta /Temp. Podemos proteger esas variables cifrando sus datos. Puede que PHP no se haya instalado con un cifrador, por lo que tendremos que agregar ese complemento. En primer lugar recordamos como instalé PHP en mi servidor local Apache. Allí veíamos que no se instalaban todos lo complementos, por lo que ahora podemos hacerlo. Como estoy en Windows voy a agregar/quitar programas y busco PHP:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida/mcrypt-1.gif)

Pulso "cambiar" y entro en la pantalla de instalación de PHP:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida/mcrypt-2.gif)

Pulso "Change" para realizar cambios en la instalación:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida/mcrypt-3.gif)

Dejo esta configuración tal como estaba (lo tenía instalado como módulo Apache 2.2) y pasamos a los complementos:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida/mcrypt-4.gif)

Los que no están instalados aparecen con una aspa roja. Buscamos el complemento mcrypt y seleccionamos para instalarlo:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida/mcrypt-5.gif)

Seguimos el resto de pasos hasta completar la instalación. Podemos ver que está instalado ejecutando la función phpinfo() y observar la parte del módulo Mcrypt con algo como esto:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida/mcrypt-6.gif)

CitarLa función phpinfo() es un recurso de PHP que compone un documento HTML con todas las configuraciones del PHP instalado. Con la instalación suele incluirse un documento llamado info.php con sólo esta función:

<?php
    echo phpinfo();
?>

Es obvio que este documento no debe estar al alcance de los usuarios de una web, pues hemos de intentar que nadie conozca cuáles son nuestras configuraciones. Por ello nunca debe incluirse en una ruta con alcance público, aunque aquí estoy probando todo esto en mi servidor local por lo que no hay mayor riesgo.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:04:47 AM
Cifrando datos en las variables de sesión

Antes de continuar conviene aclarar que muchas veces se usa el término encriptar en lugar de cifrar (o desencriptar en lugar de descifrar). El término correcto es cifrar (y descifrar), que según la R.A.E. en su primera acepción dice "transcribir en guarismos, letras o símbolos, de acuerdo con una clave, un mensaje cuyo contenido se quiere ocultar. De hecho encriptar no aparece registrado en la R.A.E., aunque si aparece el término criptografía definido como el "arte de escribir con clave secreta o de un modo enigmático".

Para poner en práctica el cifrado de datos, haremos un ejemplo de un conjunto de 3 páginas similar a los ejemplos en temas anteriores. En la primera página (pagina1c.php) iniciaremos una sesión. Luego en la página 2 se ofrece al usuario un formulario para que nos envíe algún dato que cifraremos y almacenaremos en una variable de sesión. En la tercera página accederemos a ese dato descifrándolo y devolviéndolo al usuario. Puede ver el código completo de las tres páginas.

La pagina1c.php es igual que la de los ejemplos de los temas anteriores, sólo contiene un inicio de sesión, con dos enlaces a las páginas siguientes:


//Lo primero es iniciar la sesión antes de enviar nada, pero
//estableciendo el modo de usar sesiones sólo con cookies por si
//estuviera desactivado. También damos nombre a la sesión.
ini_set("session.use_cookies", 1);
ini_set("session.use_only_cookies", 1);
session_name("sesionC");
session_start();
//Ahora comprobamos que la sesión no haya sido iniciada
if (!isset($_SESSION["iniciada"])) {
    //Esto evita la fijación de sesión
    session_regenerate_id(true);
    //Ahora asignamos el parámetro de sesión por primera vez
    $_SESSION["iniciada"]  = "si";
}
//Ahora se construye la página
?>

<!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>
...
        <li><a href="pagina2c.php">Ir a página 2c</a> para enviar una dirección de
        email y que sea cifrada y almacenada en una variable de sesión.</li>
        <li><a href="pagina3c.php">Ir a página 3c</a>, donde se recupera la dirección
        email descifrada.</li>
        ...   

   

En la pagina2c.php realizamos el cifrado y descifrado. Esquemáticamente hace lo siguiente:

<?php
...
session_name("sesionC");
session_start();
...   
//Comprobamos si la sesión ya fue iniciada
if (!isset($_SESSION["iniciada"])){//LA SESIÓN NO HA SIDO INICIADA
    //Si se ha llegado aquí es que es una sesión inesperada.
    
...DESTRUIMOS SESIÓN Y REDIRIGIMOS A pagina1c.php...
} else {
//LA SESIÓN YA FUE INICIADA
    //Vemos si existe la variable de sesión almacenada
    
if (isset($_SESSION["email"])) {
        ...
DESCIFRAMOS EL EMAIL...               
    }
    
//Si está página se está recibiendo por la ejecución del botón
    //submit del formulario, tomamos el valor del email recibido
    
if (isset($_POST["email"])){
        ...
CIFRAMOS EL EMAIL... 
    }    
}
?>


...
<html ...>
    ...
    <form action="pagina2c.php" method="post">
        <label>Email:
        <input type="text" name="email" size="50" maxlength="50"
        style="font-family: Courier New; color: maroon; "
        value ="<?php echo $un_email?>" />
        </label>
        <input type="submit" value="enviar" />
    </form>
    ...   

   

En esta pagina2c.php primero hacemos session.start para iniciar la sesión denominada "sesionC". Luego comprobamos si no hay sesión iniciada anteriormente con la variable $_SESSION["iniciada"], con lo cual sería una sesión inesperada. Esta parte es igual que lo que explicamos en el tema 2 sobre la destrucción de sesiones inesperadas.

En otro caso la sesión ya estaba iniciada. Recordemos que el ejemplo almacenará la variable de sesión $_SESSION["email"] en la carpeta /Temp de sesiones, y que nuestro propósito es cifrar esa variable para que no pueda ser leída. Por lo tanto en primer lugar debemos comprobar si existe la variable y en ese caso proceder a descifrarla e incluirla en el formulario que se acompaña en esta página. Pero si no existe la variable, comprobaremos si la hemos recibido por POST a través de ese formulario, en cuyo caso la ciframos y guardamos en la sesión. La pagina3c.php sólo realiza la parte del descifrado.

Si en pagina2c.php envíamos algo como "esto está cifrado" por el formulario, en la variable de sesión $_SESSION["email"] quedará almacenada esa frase cifrada como pHuY/TE2xvbrPWNEIKGI6S+hSGqP8wg6. Si vamos a la carpeta /Temp y buscamos el archivo de sesión (cuyo identificador vemos expuesto en la página), abriéndolo en modo texto aparecerá esto:

iniciada|s:2:"si";email|s:32:"pHuY/TE2xvbrPWNEIKGI6S+hSGqP8wg6";

En principio parece que hemos conseguido nuestro propósito: cualquiera que rebusque en /Temp sólo encontrará datos cifrados. Pero aún podemos mejorarlo. En la página con el código completo puede ver con comentarios detallados como funciona el cifrado y descifrado, por lo que no voy a extenderme en exceso aunque exponemos ahora el código de la parte de cifrado para comentar algunos detalles, con enlaces al manual PHP para más información:

...
...Nota: en la variable $un_email tenemos un dato que vamos a cifrar...
...
$cifrador = mcrypt_module_open(MCRYPT_DES, "", MCRYPT_MODE_ECB, "");
$maximo_tamanyo_vector_inicio = mcrypt_enc_get_iv_size($cifrador);
//ECB ignora el vector inicio, pero hay que ponerlo
$vector_inicio = mcrypt_create_iv($maximo_tamanyo_vector_inicio, MCRYPT_RAND );
$maximo_tamanyo_llave = mcrypt_enc_get_key_size($cifrador);
$llave_md5 = md5("1234");
$llave = substr($llave_md5, 0, $maximo_tamanyo_llave);
mcrypt_generic_init($cifrador, $llave, $vector_inicio);
$un_email_cifrado = mcrypt_generic($cifrador, $un_email);
mcrypt_generic_deinit($cifrador);
mcrypt_module_close($cifrador);
$un_email_cifrado_64 = base64_encode($un_email_cifrado);
$_SESSION["email"] = $un_email_cifrado_64;

   

En primer lugar el algoritmo usado para el cifrado es el DES en modo ECB, declarado con las constantes MCRYPT_DES y MCRYPT_MODE_ECB respectivamente al abrir el módulo cifrador. Este modo ECB cifra bloques de 64 bits de forma independiente. Por ejemplo, la cadena 12345678 tiene 64 bits, cuya cadena cifrada y sin aún codificarla en base64 sería CDCF8812BD2ECCB8 (presentada en hexadecimal). Si duplicamos la cadena inicial 1234567812345678 también se cifra igual el siguiente bloque CDCF8812BD2ECCB8CDCF8812BD2ECCB8. Si hay bloques cifrados repetidos, parece que se podrían usar técnicas que logran descifrar el mensaje. Además este modo ECB cifra el mensaje siempre de la misma forma. Para evitar ambas cosas deben usarse otros modos como CBC cuyo detalle veremos en el próximo apartado.

Pero el problema más importante es la sentencia $llave_md5 = md5("1234") donde especificamos la contraseña de cifrado, que en este ejemplo hemos puesto "1234". Si estamos intentando impedir que alguién interno a nuestro alojamiento pueda ver nuestras sesiones en /Temp, nada le impedirá ver el código fuente de este algoritmo y coger esta contraseña. Como el cifrador en modo ECB cifra siempre igual para una misma contraseña, no tendría ningún problema para descifrar el dato usando su propio código.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:05:25 AM
Detalles del descifrado

Antes de ver el siguiente apartado donde intentaremos resolver los problemas planteados, conviene detenerse en el descifrado:

...
//Vemos si existe la variable de sesión almacenada
if (isset($_SESSION["email"])) {
    //Extraemos el email cifrado y codificado en base64
    $un_email_cifrado_64 = $_SESSION["email"];
    if ($un_email_cifrado_64 != ""){
        //decodificamos base64 para dejarlo en binario
        $un_email_cifrado = base64_decode($un_email_cifrado_64);
        //Abrimos cifrador, IV, llave e inicializamos cifrador de la
        //misma forma que en la parte de cifrado (ver allí comentarios)
        $cifrador = mcrypt_module_open(MCRYPT_DES, "", MCRYPT_MODE_ECB, "");
        $maximo_tamanyo_vector_inicio = mcrypt_enc_get_iv_size($cifrador);
        $vector_inicio = mcrypt_create_iv($maximo_tamanyo_vector_inicio, MCRYPT_RAND );
        $maximo_tamanyo_llave = mcrypt_enc_get_key_size($cifrador);
        $llave_md5 = md5("1234");
        $llave = substr($llave_md5, 0, $maximo_tamanyo_llave);
        mcrypt_generic_init($cifrador, $llave, $vector_inicio);
        //Desciframos el email
        $un_email = mdecrypt_generic($cifrador, $un_email_cifrado);
        //Quitamos los nulos de relleno de la derecha
        $un_email = rtrim($un_email, "\0");
        //Finalizamos cifrador y lo cerramos
        mcrypt_generic_deinit($cifrador);
        mcrypt_module_close($cifrador);
    }       
}   
...


Si hay una variable almacenada $_SESSION["email"] la decodificamos base64 para luego descifrarla. El proceso es parecido al cifrado. Se abre un cifrador, un vector inicio, se ajusta el tamaño de la llave y se inicia el cifrador. Ahora desciframos con mdecrypt_generic y ya tenemos nuestro dato descifrado.

Hay un detalle a tener en cuenta cuando el cifrador se ve obligado a rellenar por la derecha con caracteres nulos. La cadena 1234567890 tiene 80 bits (10 bytes). Como el cifrador DES trabaja en bloques de 64 bits (8 bytes), antes de cifrar esa cadena le agrega 6 bytes a la derecha con caracteres nulos para conseguir 2 bloques completos de 64 bits. Si ciframos y desciframos esa cadena 1234567890 obtenemos en el navegador Firefox 3.6 lo siguiente:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida/fffd.gif)

La referencia de caracter &xFFFD; en UNICODE se usa para representar caracteres irreconocibles cuando estamos usando, por ejemplo, la codificación UTF-8 en este documento. El caracter es representable gráficamente si lo incluimos en un elemento HTML como <code>&xFFFD;</code> y también dentro de un <input>. Si estamos usando Firefox 3.6 podemos verlo en pantalla: �. En otros navegadores no aparecerá el caracter o bien se verá un cuadrado indicando que no reconoce el caracter UNICODE.

Para evitar esto hemos de quitar el relleno del cifrador cuando sea necesario, para lo cual usamos la función rtrim($un_email, "\0"). Así eliminamos por la derecha todos los caracteres nulos.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:06:40 AM
Cifrado con llave compartida

Los problemas señalados al final del apartado 2 eran:

    El modo de cifrado ECB no es seguro pues cifra siempre igual. Hay que usar otro modo donde el vector IV modifique el cifrado en cada vez y en cada bloque.
    Usar la contraseña o llave de cifrado en el código del script es una vulnerabilidad pues puede ser leída.

Para evitar esto componemos otro grupo de tres páginas siguiendo el mismo ejemplo. Puede consultar el código completo de estas páginas. Con este enlace pagina1d.php podemos ver en ejecución la página inicial que ahora tiene algunas cosas nuevas:

<?php
ini_set
("session.use_cookies"1);
ini_set("session.use_only_cookies"1);
session_name("sesionD");
session_start();
//Comprobamos si la sesión ya fue iniciada
$hay_sesion = isset($_SESSION["iniciada"]);
//Vemos si se está recibiendo un POST
$hay_post = (!empty($_POST));
//Vemos si se ha pulsado el botón de finalizar sesión
$finaliza_sesion = isset($_POST["fin-sesion"]);  
...
if (!
$hay_sesion) {//CON SESIONES NO INICIADAS
    
session_regenerate_id(true);
    
$llave_base md5(uniqid(rand(), true));
    
$longitud strlen($llave_base);
    
$mitad $longitud 2;
    
$llave_servidor substr($llave_base0$mitad);
    
$_SESSION["iniciada"]  = $llave_servidor;
    
$llave_cliente substr($llave_base$mitad$longitud);    
    
setcookie("llave-sesion"$llave_cliente0,  dirname($_SERVER["PHP_SELF"]));
} else {
//CON SESIONES INICIADAS
    
if ($hay_post && $finaliza_sesion){
        
$_SESSION = array();
        
setcookie(session_name(), ""time()-42000dirname($_SERVER["PHP_SELF"]));
        
setcookie("llave-sesion"""time()-42000dirname($_SERVER["PHP_SELF"]));  
        
session_destroy();
        
$host  $_SERVER["HTTP_HOST"];
        
$uri   rtrim(dirname($_SERVER["PHP_SELF"]), "/\\");
        
$pagina "pagina1d.php";
        
header("Location: http://$host$uri/$pagina");
        exit;    
    }
}
//Ahora se construye la página
?>


    ...
    <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
        <input name="fin-sesion" type="submit" value="finalizar sesión" />
    </form>
    ...


Conviene realizar una recogida de identificaciones iniciales para ver si la sesión ha sido iniciada o hay algo en el POST. Así declaramos booleanos $hay_sesion, $hay_post y $finaliza_sesion, valores que nos permitirán clarificar su manejo en el código siguiente.

En este grupo de páginas con sesión acompañaremos siempre un fomulario con sólo el botón de envío para finalizar la sesión. Aunque en este ejemplo remitimos a la página de inicio pagina1d.php con fines didácticos, en una situación práctica deberíamos finalizar la sesión y salir del grupo de páginas protegidas, enviando al usuario a una página sin sesión.

Si no hay sesión previa iniciada hacemos lo mismo que otras veces regenerando el identificador. Pero ahora agregamos el código necesario para construir la llave o contraseña para el cifrado de datos. En primer lugar construimos una $llave_base usando la función PHP uniqid() que genera una cadena identificadora única basada en el reloj. El primer argumento es un número aleatorio generado por la función PHP rand(), evitando posible generación del mismo identificador por varios sitios que comparten el mismo reloj, como es el caso del alojamiento compartido. El segundo argumento es true que agrega entropía adicional lo que hará que los resultados sean más únicos.

En definitiva se trata de generar una cadena de 23 caracteres que nos servirá de contraseña o llave para cifrar nuestros datos. Luego obtenemos la firma (denominado también valor resumen, valor hash, signatura, etc.) de la llave base con el algoritmo MD5, con lo cual tenemos una cadena de 32 caracteres (256 bits). Esta aún no es la llave final, pues ahora la dividiremos en dos partes. La primera mitad ($llave_servidor) la guardaremos en la variable de sesión $_SESSION["iniciada"] y la otra mitad ($llave_cliente) la enviaremos con una cookie al usuario con setcookie("llave-sesion", ...). Cuando vayamos a cifrar o descifrar algo en la página 2 y siguientes hemos de reunir las dos partes concatenando ambas mitades y aplicando la firma de nuevo md5($_SESSION["iniciada"].$_COOKIE["llave-sesion"]). Usar la firma otra vez es importante pues esa concatenación tiene 256 bits y la contraseña que necesita el algoritmo DES es de sólo 64 bits (56 útiles). Como se toman los primeros caracteres hemos de pasar otra vez la firma pues con sólo la media llave del servidor nos serviría para descifrar el mensaje.

Se observa que si se recibe el botón de finalizar sesión, lo cual se detecta con $hay_post && $finaliza_sesion, entonces destruimos la sesión como en otros ejemplos. Además también anulamos la cookie del navegador donde está almacenada la media llave del cliente.

En la pagina2.php hacemos el cifrado de un dato recibido desde un formulario. En el código de esa página puede ver todos los detalles, exponiéndo a continuación sóo lo que nos interesa comentar sobre cifrado:

...
if (!$hay_sesion || ($hay_sesion && $hay_post && $finaliza_sesion)){
    //LA SESIÓN NO HA SIDO INICIADA O FINALIZAMOS SESIÓN
    ... aquí se destruye sesión ...
} else  {
    //LA SESIÓN YA FUE INICIADA
    if (isset($_COOKIE["llave-sesion"])){
        $llave_doble = md5($_SESSION["iniciada"].$_COOKIE["llave-sesion"]);
        //Vemos si existen las variables de sesión almacenadas
        if (isset($_SESSION["email"]) && ($_SESSION["email"] != "")
            && isset($_SESSION["iv"])) {
            //Extraemos el email cifrado y codificado en base64
            ... aquí se extrae y descifra el email que está en $_SESSION["email"] ...
        }
        //Recogemos el email recibido
        if (isset($_POST["email"])){
            $un_email = htmlspecialchars($_POST["email"], ENT_QUOTES);
            ...
            //CIFRAMOS
            $cifrador = mcrypt_module_open(MCRYPT_DES, "", MCRYPT_MODE_CBC, "");
            $maximo_tamanyo_vector_inicio = mcrypt_enc_get_iv_size($cifrador);
            $vector_inicio = mcrypt_create_iv($maximo_tamanyo_vector_inicio, MCRYPT_RAND);
            $_SESSION["iv"] = base64_encode($vector_inicio);;
            $maximo_tamanyo_llave = mcrypt_enc_get_key_size($cifrador);
            $llave = substr($llave_doble, 0, $maximo_tamanyo_llave);
            mcrypt_generic_init($cifrador, $llave, $vector_inicio);
            $un_email_cifrado = mcrypt_generic($cifrador, $un_email);
            mcrypt_generic_deinit($cifrador);
            mcrypt_module_close($cifrador);
            $_SESSION["email"] = base64_encode($un_email_cifrado);
            ...
        }       
    }
}

   

En el código del ejemplo del apartado 2 se explica con detalle en los comentarios cada una de las sentencias involucradas en un cifrado. El descifrado es similar. De todas formas volvemos a repetir los pasos del cifrado para este ejemplo:

    Abrimos un cifrador con mcrypt_module_open, usando el algoritmo DES y el modo CBC por medio de las constantes señaladas.
    El modo CBC requiere un vector de inicialización (IV: initialization vector), pero primero obtenemos el tamaño máximo de ese vector para el algoritmo que estamos usando, lo cual conseguimos con mcrypt_enc_get_iv_size($cifrador).
    Creamos un IV con mcrypt_create_iv aplicándole ese tamaño máximo y la constante MCRYPT_RAND si estamos en PHP bajo Windows (en otro caso consulte el manual PHP).
    Ese IV lo codificamos en base64 para almacenarlo en la variable de sesión $_SESSION["iv"].
    Obtenemos el máximo tamaño de la llave para ese algoritmo, lo que se consigue con mcrypt_enc_get_key_size($cifrador).
    Nuestra llave doble es mayor que el tamaño que necesitamos, por lo que la recortamos con substr.
    Iniciamos el cifrador con mcrypt_generic_init aplicándole la llave y el vector de inicialización.
    Ciframos nuestro dato con mcrypt_generic.
    Finalizamos el cifrador con mcrypt_generic_deinit.
    Cerramos el módulo cifrador con mcrypt_module_close.
    Guardamos el dato cifrado en $_SESSION["email"] después de haberlo codificado en base64.

En resalte azul hemos señalado algunas cosas. Primero vemos como se recompone la $llave_doble si encontramos la cookie con la media llave que se envió al cliente. Esta será la contraseña para descifrar $_SESSION["email"] o cifrar $un_email que se haya recibido por POST. El algoritmo DES se abre en modo CBC. Así evitamos bloques cifrados iguales. Este modo requiere que se guarde el vector de inicialización (IV) para luego ser usado en el descifrado. Podemos almacenarlo en una variable $_SESSION["iv"]. Lo codificamos en base64 para no incluir caracteres de control en el archivo de sesión. En la ejecución de pagina2d.php se observa que cada vez que se envía el dato, éste es cifrado de forma distinta. Sucede esto porque se genera un IV diferente en cada apertura del cifrador dando lugar a un cifrado distinto. Aunque es una mejora de seguridad, obliga a tener que almacenar ese IV en algún sitio para usarlo en el descifrado.

Si enviamos la cadena "esto está cifrado" en esa pagina2d.php, obtenemos esta captura de pantalla:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-protegida-2/muestra.gif)

En ese momento esa cadena cifrada es KddCTD+Yh24puQchmhskre6HO+tjtgF0, pero si la envíamos de nuevo obtendremos otro ciframiento. En la carpeta de sesiones /Temp encontramos el archivo de sesión con este contenido:

iniciada|s:16:"b2ab7f14b040b836";
iv|s:12:"aQT4gWAYOCU=";
email|s:32:"KddCTD+Yh24puQchmhskre6HO+tjtgF0";

   

Si alguién mira este archivo sólo podrá acceder a la media llave del servidor y al IV. Sin la otra media llave del cliente no podría descifrar el email. Ya no tenemos el problema planteado de que alguién pudiera tomar la contraseña escrita en el código de nuestro script. Sin embargo se ha de tener en cuenta que se envía al cliente la media llave a través de una cookie. En las peticiones del cliente siempre acompañará esta cookie, por lo que este mecanismo no se libra de que alguién pueda interceptarla y obtener la media llave. Además si se recibe el dato por POST para luego ser cifrado, también puede ser interceptado antes de llegar al servidor. E incluso en la pagina3d.php volvemos a enviar el email descifrado al cliente.

En definitiva, este mecanismo sólo serviría para proteger los datos de las variables de sesión desde la perspectiva de que otros sitios del alojamiento compartido pudieran verlas. Y si además la sesión estuviera finalizada porque el usuario cerró el navegador, aunque aún existiera la sesión en /Temp, ya no habría forma de recuperar la otra media llave del cliente para descifrar esas variables, pues la cookie ya no existiría en el navegador del cliente. Es desde este punto de vista en que esta estrategia podría ser interesante, puesto que si estamos buscando cifrar todo el flujo de datos tendremos que ir a técnicas más complejas como el protocolo HTTPS.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:15:31 AM
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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:18:04 AM
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 propaga-sesiones.zip (70 KB) con todos los códigos en formato de archivos de texto plano (con extensión cambiada a "txt").
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:19:38 AM
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.

<?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:

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:

$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):

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:

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:

$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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:20:23 AM
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):

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:

...
<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:

$_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:

...
<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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:21:01 AM
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:

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:

...
<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:

$_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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:21:41 AM
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:

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:

...
<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:

...
<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).
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:22:18 AM
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:

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:

...
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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:22:49 AM
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:

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:

...   
<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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:24:09 AM
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:

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:

<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:

<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:

<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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:25:10 AM
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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:25:38 AM
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 paginas.zip. 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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:27:34 AM
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:

$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:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-registrada/autentica1.gif)

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:

<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:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-registrada/autentica2.gif)

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,
<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:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-registrada/autentica4.gif)

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:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-registrada/autentica5.gif)

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:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-registrada/autentica6.gif)

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:

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.
Título: Re:Sesiones
Publicado por: Alex en Enero 31, 2013, 12:29:25 AM
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:

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:

(http://www.wextensible.com/temas/php-sesion/ejemplos/sesion-registrada/autentica7.gif)

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:

$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

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

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

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:

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:

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:

$_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:

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:

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):

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:

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.