Los últimos días los he dedicado al estudio de uno de esos viejos problemas que nunca había podido resolver. Consiste en el almacenamiento «seguro» de datos que viajan en un canal inseguro y se almacenan en un medio inseguro. Puntualmente, hablo de COOKIES. Cuando hablamos de protocolo HTTP(S), las cookies son el único «repositorio de datos» o almacenamiento persistente en el lado del cliente (navegador) con el que podemos trabajar. En las cookies, podemos guardar las preferencias de un usuario (por ejemplo, el idioma que escoja, la última página que vió, etc…). Y el problema puntual con cookies: guardar la identidad del usuario para «recordar» su sesión. El peligro esta en el como se guarda esta identidad. Lo bueno es que ya encontré un método que me convenció, y realmente es bastante «simple» como algoritmo. Lo que no fue simple fue la implementación. La verdad es que no se como pretendía completar el primer punto de mi roadmap (Lista de control de acceso o ACL) sin tener primero un método decente de autenticación. Al menos ya di con una respuesta.
El problema de la autenticación por cookie
Para autenticar al usuario, necesitamos información de este que no diga «ah! este usuario es Perico Los palotes y no Pedro Juan Diego». El problema se reduce entonces a «que dato guardar en la cookie, que sea único por cada usuario».
El nombre de usuario o IDentificador
Una solución simple es, por ejemplo, guardar el nombre de usuario o un número «ID». Esto sería una solución válida, de no ser por lo extremadamente inseguro que significa. Si guardo solo el nombre o id del usuario, cualquier persona podría crear una cookie en su navegador con la identidad de otra persona. Finalmente entraría a la web y ya estaría autenticado. Más que inseguro, es extremadamente estúpido.
El nombre de usuario + clave
Para obstaculizar un poco un eventual ataque, podríamos conciderar añadir un segundo campo, con la contraseña del usuario. Pero nuevamente, sería desastroso en términos de seguridad. Ahora la información de la sesión, estaría a la vista de cualquier persona que ocupe físicamente el mismo equipo. O peor, un atacante remoto que haya podido ver la cookie (vía malware o interceptando la comunicación). Este atacante podría iniciar la sesión por usuario y contraseña sin problemas. Otra solución, consiste en encriptar la clave y desencriptarla en el servidor, pero solo sería una distracción, porque el atacante igualmente podría clonar la cookie y autenticarse, a pesar de estar encriptada la contraseña. Lo mismo pasaría si en vez de encriptarla, se contara con una segunda «clave», que solo fuera válida por cookie. No sería posible iniciar sesión de forma clásica, pero si se podría directamente copiando la clave. Entonces ¿que guardar en la cookie para autenticar al usuario? Hace años, cuando realizé esta pregunta en diversos foros, recibí como respuesta que no había solución perfecta, pero si quería hacer algo que ofusque un ataque.
Contrastar una clave aleatoria con una clave en la base de datos
Buscando una solución, llegue a una implementación de Cookies de Autenticación, consistente en un sistema de login. En esa implementación, se pone el enfasis de la seguridad en la IP del usuario. Pero obviando ese dato, el algoritmo combina el id de sesión de PHP, las cookies y la base de datos, de tal modo que entre esos 3 medios de almacenamiento se guarden «claves», las cuales se contrastarán entre los 3 para autenticar los datos. No es del todo malo, pero resta rendimiento, debido a que hace consultas extras a la base de datos y muchas comprobaciones. Lo contraproducente del asunto, es que el diseño de este algoritmo impediría que un usuario guarde su sesión en más de un navegador, porque cada vez que se genere la clave aleatoria nueva, se pierde la validez de las cookies guardas antes en otros navegadores. Peor aún, si incluímos la IP como un dato para comprobar la validez de la cookie, no servirá de nada con usuarios que tengan conexiones a internet con IP dinámica. ¿Entonces, el problema tiene solución?
Una solución elegante
Leyendo más acerca de este problema en general, mirándolo desde afuera de la visión sesgada de PHP, logré encontrar un paper muy interesante (solo 3 páginas). Parte planteando las mismas interrogantes y falencias sobre otros algoritmos como los que me había planteado, con la diferencia de que propone soluciones a cada uno de los problemas y los combina en un solo algoritmo. Más interesante aún, es que el paper describe el método completo, pero no se ciega en un lenguaje en particular donde implementarlo (navegando encontré incluso que fue implementado en python). Por si el hecho de que los autores del paper son doctores en computación y expertos en criptografía fuera poco, una de las cosas que me dio confianza (aunque suene burdo), es que el mismo wordpress y otros sistemas estarían usando este método de autenticación por cookies. Haciendo otra búsqueda, me tope con alguien que lo implemento en PHP, y además implementó un adaptador para Zend_Auth. Finalmente, tomé prestado el código de ambas clases, la manejadora de cookies seguras y la adaptadora para Zend_Auth), y las incorporé en mi proyecto. Arregle uno que otro defecto que tenían:
- Ya no requiere mcrypt (que a veces no está instalado en los hostings). La característica de alta-confidencialidad es opcional y funciona solo con mcrypt activado, sino por defecto usa el modo de baja-confidencialidad.
- El dato de entrada debía ser un string. Ahora puede ser cualquier dato soportado por php, excepto «recursos». Es decir, se pueden almacenar incluso objetos.
Hay una mejora que aún no implemento, que consiste en aplicar un hash al campo «username», de esta forma, aumenta un poco más la confidencialidad del usuario, debido a que un administrador o un atacante no podría saber exactamente «quien fue el que inicio la sesión en el sitio» alguna vez, tan solo mirando las cookies. De todos modos, la identidad y rol los guardaría en el campo «data» serializados y codificados (y si es posible, encriptados también).
Y para rematar… ¿PHP Bug?
Como en prácticamente todos mis desarrollos, me he topado con Expedientes X. En esta ocasión, descubrí un comportamiento medio curioso con la serialización de obejtos. La serialización de variables permite convertir el valor y el tipo del dato (aunque sea compuesto como un array o un objeto) en una representación «almacenable» (en este caso, una cadena de texto). Antes, no había tenido problemas con esto, pero ahora, para implementar el algoritmo de Cookie Segura, me vi forzado a usar serialización. Lo que no comprendo del todo, es porque al serializar objetos con atributos protegidos o privados, aparecieron unos caracteres raros entremedio. Curiosamente al pasar la misma cadena por otras funciones (como base64_encode/decode y las mcrypt), finalmente obtenía la misma cadena serializada, pero esta vez limpia de esos caracteres raros. Extrañamente, no provocó ningún efecto adverso al deserializar los datos, por lo tanto, conservaban su integridad como era esperado. Lo malo del asunto, es que el algoritmo de cookies seguras, requiere usar el campo «data» guardado en la cookie para formar la «llave» que comprueba la validez de la cookie. Si la cadena obtenida al desencriptar el dato de la cookie retorna «limpio», entonces obviamente no concuerda con la cadena «sucia» que formo la llave inicial, y por lo tanto resulta en una cookie inválida, aún cuando el dato estaba bueno. Si alguien desea probar si «su php» también tiene hipo, pueden probar este sencillo script.
class data
{
public $a = '123';
protected $b = 'abc';
private $c = 'xyz';
}
$data = new data();
var_dump( $data ); // Can you see a fail?
En algunos casos, al parecer la salidad es correcta, es decir, sin ningún caracter raro metido entremedio. En otros casos (como el mío y otros más que me ayudaron comprobándolo), sí aparecían bichos en la salida. Una de las sospechas, recaen en que los que tuvimos la falla, estaríamos ocupando un «locale» en español en el sistema, encambio los que no tienen el fallo, usaban su «locale» en inglés. Espero (nuevamente) que el futuro motor de PHP6 deje de una vez de ser tan sencible a los cambios de «encoding», tanto por el idioma del sistema en que corre, como por los scripts de entrada y por el texto plano de salida. No se en realidad a que se debe esto, incluso sospeche que esto se trataba de un bug en ubuntu, pero finalmente resultó no ser tal. Finalmente, mi «workaround» consistió en modificar aún más el algoritmo, de tal modo que la llave la formé con un hash del objeto serializado, en vez del objeto serializado por sí solo. Esto agrega unos cuantos milisegundos y operaciones extra (despreciables en todo caso), pero al menos logré «universalizar» el mismo script, que era el objetivo primario. Al menos, creo que aprendí bastantes cosas tratando de resolver este poblema.
«Y así es como Gonzalo Díaz inició la búsqueda de su propio Santo Grial». Se podría empezar a escribir una novela con este post.
:p
Saludos.
Está buenisimo el método, lo voy a empezar a utilizar. Siempre con buenos datos tu
hay algo que no entiendo, aunque esten serializados los datos, si yo agarro y me copio la cookie y me la llevo , cuando los deserialize va a andar igual. no entiendo mucho donde esta la seguridad en eso. a menos que pongas que la cookie caduca en x tiempo.
Saludos
Disculpen mi ignorancia, pero si la configuración del explorador del usuario no acepta cookies o es muy restrictiva en cuanto a seguridad ¿Qué ocurre o qué hacer?
¿Esto puede darse en muchos casos?
Buen post compañero.
Un saludo.
Diganme ciego loco o sordomudo pero no encuentro el enlace al codigo que lograste hacer con todo lo que investigaste, o es que nunca se compartio??
Estoy investigando un poco al respecto para implementar esto….
Buen post, pregunto: serias capaz de compartir el código ? 🙂 jejeje
excelente post!…sólo quería agregar que yo también me encontré con el «problema» de los caracteres raros que agrega php a las propiedades pivadas o protegidas, aunque en mi caso estab probando convertir objetos a arreglos, lo que me dí cuenta es que algunos no son visibles (caracteres de control) y lo «solucione» (entre comillas, porque no me pareció una buena solución) recorriendo con un bucle foreach el arreglo y copiandolo a otro arreglo, aunque al momento de establecer la clave, le aplicaba un filter_var()