Le sessioni vengono normalmente salvate da PHP su file ma, come abbiamo visto anche nel nostro articolo Sessioni PHP: cosa sono come si usano, sono disponibili anche dei meccanismi di memorizzazione alternativi detti 'handler' che sono generalmente associati a particolari estensioni.
In questo articolo vedremo come configurarli ma soprattutto come creare handler personalizzati per poter salvare i dati delle nostre sessioni sui supporti più disparati e secondo le nostre esigenze in modo del tutto trasparente rispetto agli applicativi che li andranno ad utilizzare.
In questo articolo vedremo, oltre a creare handler di sessione personalizzati, anche le istruzioni per gestire le sessioni con sistemi alternativi. In particolare, vedremo come:
- Gestire le sessioni PHP con APC
- Gestire le sessioni PHP con Windows Wincache for PHP
- Gestire le sessioni PHP con Memcached
- Gestire le sessioni PHP con SQLite e MySQL
I session handler ed il PHP 5.4
Prima di iniziare è bene ricordare che con la versione 5.4 di PHP è stata introdotta un'interfaccia SessionHandlerInterface ed un'implementazione (solo dal punto di vista formale però) nella classe SessionHandler. L'idea è la stessa di questo articolo, implementare cioè gli handler estendendo SessionHandler o implementando direttamente l'interfaccia SessionHandlerInterface.
La registrazione degli handler siffatti può essere eseguita sempre attraverso la funzione session_set_save_handler()
passando uno o due parametri: il primo è l'handler ed il secondo è un booleano che se true registra come funzione di chiusura il comando session_write_close()
attraverso il comando register_shutdown_function().
C’è da dire che vengono proposte solo delle strutture. Tutte le implementazioni sono a carico dell’utilizzatore; in più, essendo basate su metodi dinamici, soffrono delle problematiche che hanno portato alla scelta usare metodi statici in questo articolo, come vedremo di seguito.
Come creare degli handler di sessione personalizzati
La definizione di un handler personalizzato è consentita attraverso la funzione: session_set_save_handler(), nell'uso classico occorre passare a questa funzione sei parametri che corrispondono ai nomi delle funzioni definite dall'utente che dovranno essere invocate sul verificarsi dei sei eventi cardine del gestore delle sessioni:
- Open: Apertura sella sessione, che corrisponde all'invocazione del comando session_start(). La funzione incaricata di gestire questo evento riceve in ingresso due parametri che corrispondono ai parametri del php.ini 'session.save_path' e 'session.name'
- Read: Lettura dei dati di sessione da un sistema di archiviazione, che è un evento scatenato dal temine dell'evento 'Open' . La funzione incaricata di gestire questo evento riceve in ingresso il session ID e deve restituire una stringa che contiene l'ultima versione salvata dell'array
$_SESSION
nella forma serializzata - Write: Scrittura dei dati nel sistema di archiviazione , che corrisponde all'invocazione di
session_write_close
o viene eseguito termine dello script se la sessione non è chiusa esplicitamente. La funzione incaricata di gestire questo evento riceve in ingresso il session ID ed una stringa con l'array $_SESSION serializzato che deve essere salvato - Close: Chiusura della sessione, che è un evento scatenato dal temine dell'evento 'Write'.
- Destroy: Distruzione dei dati della sessione, che corrisponde all'invocazione del comando
session_destroy()
. La funzione incaricata di gestire questo evento riceve in ingresso session ID ed elimina tutti i dati relativi a quella sessione - GC: Pulizia delle sessioni scadute eventualmente invocata dopo open secondo criteri definiti nel php.ini nei parametri
session.gc_probability
esession.gc_divisor.
La funzione incaricata di gestire questo evento riceve in ingresso un intero pari al valore session.gc_maxlifetime ed elimina le sessioni "scadute", quelle cioè a cui non si accede da un numero di secondi superiore al parametro passato
Prima di cominciare a scrivere le funzioni per implementare un handler di sessioni personalizzato, vediamo come si può standardizzare questo procedimento tramite una classe astratta da estendere di volta in volta a seconda delle tecnologie le strategie scelte.
<?
abstract class mySessionHandler {
/**
* Registrazione dei metodi open,gc,close, read,write,destroy
* della classe che li implementa */
public static function set_handler() {
//ricava il nome della classe che invoca il metodo
$classe = get_called_class();
//utilizziamo come parametri della funzione session_set_save_handler()
//le chiamate ai metodi statici astratti definiti in questa classe
session_set_save_handler("$classe::open", "$classe::close", "$classe::read", "$classe::write", "$classe::destroy", "$classe::gc");
}
/* Metodi di gestione da implementare */
public static function open($path, $name) {
}
public static function read($sid) {
}
public static function write($sid, $dati_sessione) {
}
public static function destroy($sid) {
}
public static function gc($maxlifetime) {
}
public static function close() {
}
}
?>
Concentriamoci ora sul metodo setHandler()
: in esso troviamo l'istruzione $classe=get_called_class()
che ci permette di ricavare il nome delle classe dell'istanza che sta invocando il metodo e che verrà usata per costruire i nomi dei metodi da passare a session_set_save_handler
. L'uso del comando get_called_class()
al posto della costante __CLASS__
è dovuto al fatto che __CLASS__
avrebbe restituito in tutte le estensioni di classe sempre il valore mySessionHandler.
La scelta di usare solo metodi statici è dovuta alle seguenti motivazioni:
- Il gestore delle sessione in ogni script è generalmente uno, laddove per qualche ragione se ne dovessero creare più istanze sarebbe comunque opportuno riciclare sempre la stessa, di fatto quindi un singleton che non vale la pena implementare in questo caso;
- per accedere ai metodi dinamici di una classe occorre istanziarla e mantenere "viva" tale istanza conservando sempre una variabile associata ad essa, pur non avendo mai la necessità di usarla direttamente poiché i metodi sono richiamati da PHP su determinati eventi;
- in fase di shutdown il PHP elimina le istanze in ordine inverso a quello di istanziazione e quindi l'istanza che gestisce le sessioni potrebbe essere distrutta prima di un'altra istanza che prevedeva modifica di $_SESSION nel distruttore vanificando così gli effetti di queste modifiche, e questo vale anche se si usa un Singleton;
- l'accesso ai metodi statici è generalmente più performante rispetto a quelli dinamici seppure in questo caso sarebbe un vantaggio del tutto trascurabile.
Gestire le sessioni PHP con APC
APC (Alternative PHP Cache) si sta sempre più affermando come estensione per il caching degli script. Un'ulteriore caratteristica di questa estensione è quella di fornire dei comandi primitivi per la gestione di RAM condivisa tra gli script.
Ecco un esempio di handler di sessioni realizzando estendendo mySessionHandler ed usando l'APC per memorizzare i dati:
<?
Class APC_mySessionHandler extends mySessionHandler{
public static function read($sid){
return apc_fetch(get_called_class().'_'.$sid);
}
public static function write($sid, $dati_sessione){
apc_store(get_called_class().'_'.$sid,$dati_sessione, ini_get('session.gc_maxlifetime'));
}
public static function destroy($sid){
apc_delete(get_called_class().'_'.$sid);
}
}
?>
Come si può notare i metodi open()
, gc()
e close()
sono del tutto inutili nel contesto APC che non prevede connessioni e/o disconnessioni ed ha un garbage collector interno e quindi non ne è stato fatto l'overriding.
Meritano di essere evidenziati:
- La scelta di usare come chiave per la memorizzazione delle sessioni la stringa composta dal SID seguito dall'underscore seguito dal nome della classe invocata per evitare eventuali conflitti con chiavi eventualmente usate in altri contesti.
- Il secondo parametro passato al comando
apc_store
nel metodowrite()
indica come timeout proprio il valore del parametro del php.inisession.gc_maxlifetime
, in modo da sfruttare il garbage collector interno all'APC per la pulizia delle sessioni scadute
Per attivare il nuovo handler basterà fare precedere il session_start()
da APC_mySessionHandler::setHandler()
ad esempio, supponendo di aver conservato tutte le classi in un unico file mySessionHandlers.php possiamo scrivere:
<?
include("./mySessionHandlers.php");
APC_mySessionHandler::setHandler();
session_start();
//resto del codice
?>
Per chi usa Zend Server e vuol sfruttare l'analoga estensione Zend data cache può non essere necessario implementare una versione ad hoc dell'handler in quanto di default la Zend data cache è in grado di simulare la presenza di APC e quindi si può usare direttamente questa stessa classe; anche se sarebbe consigliabile i comandi in quanto le primitive della Zend data cache sono in grado di gestire dei namspaces delle chiavi memorizzate ed il tutto risulterebbe più pulito.
Carichiamo i nostri handler in modo trasparente
Ma come si può caricare un handler personalizzato in modo trasparente, facendo in modo che i nostri script lo usino senza neanche accorgersene e soprattutto senza dover minimamente mettere mano al codice preesistente?
Per queste evenienze ci viene in aiuto la direttiva auto_prepend_file
del php.ini che indica uno script da caricare prima dell'esecuzione di ogni script.
Quindi supponendo di aver salvato il seguente codice nel file con questo percorso /www/protected/sessionhandler.php:
<?
include(__DIR__."/mySessionHandlers.php"); //assumiamo che anche il file con il codice della classe risieda in /www/protected/
APC_mySessionHandler::setHandler();
//non si inserisce qui il session_start() ma sarà eventualmente lo script in esecuzione a doverlo fare
?>
Semplicemente inserendo nel php.ini la riga auto_prepend_file='/www/protected/sessionhandler.php'
avremo precaricato ed attivato il nostro handler di sessioni APC automaticamente.
Gestire le sessioni PHP con Windows Wincache for PHP
Negli ultimi anni il PHP ha sempre più trovato trovando spazio su IIS, questo grazie anche all'inatteso interessamento di Microsoft che ha sviluppato in collaborazione con Zend Technologies un modulo fastcgi in grado di far girare PHP su piattaforma Windows a prestazioni più che soddisfacenti. Ma il colosso di Redmond non si è fermato qui, ha dedicato un'intera sezione del sito di IIS al mondo PHP nella quale, oltre a forum e tutorial, mette a disposizione la "Web Platform for PHP".
Questo tool permette di scaricare ed installare su IIS, oltre a famosi CMS come WordPress, Drupal o Joomla, anche una versione di PHP potendo disporre di una comoda interfaccia grafica di configurazione del php.ini (il PHPManager) e delle estensioni PHP sviluppate interamente da Microsoft quali la nuova libreria di connessione a SQL Server ed una di caching degli script del tutto analoga ad APC: la Wincache for PHP. Quest'ultima viene presentata coma una valida e più performante alternativa ad APC per le installazioni su Windows e mette anche a disposizione un handler nativo delle sessioni, è sufficiente modificare i php.ini come segue:
session.save_handler = wincache
session.save_path = C:inetpubtempsession
Da notare la presenza di una cartella nella quale l'handler salva i file temporaneamente per evitare perdite di dati durante il riciclo degli "application pool", ma se il nostro IIS è impostato in modo da non riciclare mai gli "applicaction pools" come si può fare ad evitare questo inutile spreco di tempo e spazio sul disco?
Ecco come si può facilmente scrivere un handler di sessioni basato su Wincache salvata esclusivamente in RAM..
<?
Class WinCache_mySessionHandler extends mySessionHandler{
public static function read($sid){
return wincache_ucache_get(get_called_class().'_'.$sid);
}
public static function write($sid, $dati_sessione){
wincache_ucache_set(get_called_class().'_'.$sid,$dati_sessione, ini_get('session.gc_maxlifetime'));
}
public static function destroy($sid){
wincache_ucache_delete(get_called_class().'_'.$sid);
}
}
?>
Gestire le sessioni PHP con Memcached
Finora sono state prese in considerazione le metodologie di memorizzazione dei dati di sessione più performanti, basate cioè su ram condivisa tra gli script, ma adatte esclusivamente ad un uso singolo server. p>
In condizioni di load balancing tra più server le richieste di uno stesso client possono essere indirizzate a più server alternativamente (a meno che non si configuri in modo diverso il bilanciatore) ed i dati di sessione contenuti in un server non sarebbero disponibili quando il client viene indirizzato sull'altro e viceversa.
Con l'handler di sessioni basato su file system tale inconveniente viene generalmente risolto facendo puntare 'session.save_path' ad una share di rete ma è prestazionalmente proibitivo ed in assenza di apparati sufficientemente performanti potrebbe persino annullare il vantaggio del load balancing. Per questa ragione si può installare su una delle macchine, o su un server terzo, un servizio memcached a cui tutti i server faranno riferimento come deposito di sessioni.
PHP possiede già un handler di sessioni specifico per memcached. Anzi, ne possiede due: uno per l'estensione memcache ed uno per l'estensione memcached ed è possibile attivarli direttamente da php.ini
;indicare qui memcache o memcached a seconda dell'estensione che si vuole usare
session.save_handler = memcache
;indicare qui IP:PORTA del/dei server memcached ed eventuali parametri
session.save_path= "tcp://192.22.22.22:11211"
Il collegamento può avvenire via tcp, udp o socket unix ed è anche possibile usare più server memcache in parallelo definendo politiche di connessione attraverso opportuni parametri.
Avere un'handler memcached fatto in casa può permettere di inserire dei prefissi alle chiavi per evitare eventuali collisioni nel caso il servizio non sia dedicato a memorizzare solo le sessioni oppure può permettere di gestire l'evento di mancata connessione alla memcached inviando una mail ad un sistemista e/o lanciando un'eccezione come nell'esempio seguente:
<?
class memcache_mySessionHandler extends mySessionHandler{
protected static $memcache;
public static function open($path,$name) {
self::$memcache=new memcache();
$host=explode(':',$path,2); //assumendo che $path contenga un indirizzo nella forma 192.22.22.22:11211 lo scompone
$connesso=@self::$memcache->pconnect($host[0],($host[1]?$host[1]:11211)); //se il secondo parametro non è valorizzato lo setta a 11211
if(!$connesso) {
mail('administrato@myhost.it','Memcached non attiva','Attenzione memcached non risponde alle ore '.date('H:i:s'));
throw new Exception('Errore memcache');
}
}
public static function read($sid){
return self::$memcache->get(get_called_class().'_'.$sid);
}
public static function write($sid, $dati_sessione){
self::$memcache->set(get_called_class().'_'.$sid,$dati_sessione, 0, ini_get('session.gc_maxlifetime'));
//il terzo parametro a 0 indica la non compressione del dato
}
public static function destroy($sid){
self::$memcache->delete(get_called_class().'_'.$sid);
}
public static function gc($maxlifetime){} //provvederà memcached a eliminare variabili scadute
public static function close(){//la chiusura effettiva non avviene, ma si rilascia per l'assegnazione ad eventuali altri script
self::$memcache->close();
}
}
?>
Il parametro $path può essere della forma IP (es. 127.0.0.1) o IP:porta (127.0.0.1:11211) e viene usato per indicare al nostro handler a quale servizio memcached connettersi; in questo modo tale informazione potrà essere impostata direttamente nel php.ini modificando il parametro session.save_path
oppure da programma tramite il comando ini_set()
; in alternativa si può sempre fare un'ulteriore estensione, come nel seguente esempio che sfrutta un servizio memcached locale ed ignora del tutto i parametri passati.
<?
class local_memcache_mySessionHandler extends memcache_mySessionHandler {
public static function open($non_usato1,$non_usato2) {
self::$memcache=new memcache();
$connesso= self::$memcache->pconnect('127.0.0.1',11211);
if(!$connesso) {
mail('administrato@myhost.it','Memcached non attiva','Attenzione memcached non risponde alle ore '.date('H:i:s'));
throw new Exception('Errore memcache');
}
}
}
?&?>
Occorre tenere presente che normalmente memcached non permette la memorizzazione di variabili più grandi di 1 MB, nessuno si sognerebbe mai di generare variabili di sessione tanto grandi, me se non si è certi di cosa fanno gli script sul proprio server, ove disponibile, sarebbe consigliabile nel comando set()
, invocato all'interno del metodo write()
, passare come secondo parametro MEMCACHE_COMPRESSED
anziché 0, in questo modo i dati verranno compressi prima della memorizzazione in modo del tutto trasparente avendo molto più spazio disponibile (anche 10 volte) al costo di qualche microsecondo durante compressione e decompressione.
Gestire le sessioni PHP con SQLite e MySQL
Uno dei metodi più robusti e diffusi per gestire le sessioni (seppure non altrettanto efficiente della memcached) è quello di memorizzarle in un DB. Abilitando l'estensione SQLite è possibile usufruire di un handler predefinito per la gestione delle sessioni basato su DB, basterà infatti modificare queste due righe nel file php.ini:
session.save_handler = sqlite
session.save_path = "/cartellanascosta/sessioni.db"
per far sì che tutte le sessioni vengano registrate nel DB SQLite presente al percorso /cartellanascosta/sessioni.db.
Pur essendo estremamente efficiente e performante, SQLite potrebbe non dare le sufficienti garanzie di sicurezza e, soprattutto, scalabilità dei nostri sistemi su più server.
In caso di sistemi scalati su più server l'uso di un DB locale per memorizzare le sessioni ripresenterebbe il problema tipici degli handler basati su file o ram locale, vedremo quindi come usare un server MySQL che coniuga affidabilità, efficienza, sicurezza e scalabilità.
Prima di tutto creiamo la tabella destinata a memorizzare le nostre sessioni, che per carenza di fantasia chiameremo 'sessioni', e posizioniamola in un apposito schema che chiameremo 'sistema' al quale tutti gli script avranno diritto di accedere in lettura e scrittura.
CREATE DATABASE IF NOT EXISTS `sistema`;
USE `sistema`;
CREATE TABLE IF NOT EXISTS `sessioni` (
`sid` varchar(36) NOT NULL COMMENT 'Deve contenere il valore del SID, quindi accertarsi che sia sufficientemente lungo da contenerlo tutto',
`dati` blob NOT NULL COMMENT 'Dati della sessione',
`time` int(10) unsigned NOT NULL COMMENT 'Ultima modifica della sessione in formato UNIX_TIMESTAMP(), usato per definire la scadenza delle sessioni',
PRIMARY KEY (`sid`)
);
Come si può notare si tratta di una semplice tabella con tre colonne, il SID (che è anche chiave primaria), i dati della sessione, ed un valore intero positivo che tiene dell'ultimo accesso alla sessione espresso in secondi
Vediamo ora una classe che può usare questa tabella basandoci per generalità sui comandi primitivi del MySQL.
<?
Class mysql_mySessionHandler extends mySessionHandler{
protected static $mysqlconn;
public static function open($path,$name) {
$pars=explode('/',$path,3); //assumendo che $path sia della forma host/user/pass la scompone in $pars
self::$mysqlconn=mysql_pconnect($pars[0],$pars[1],$pars[2]); //non aggiungo mysql_select_db perché espliciterò sempre lo schema nelle query if(!self::$mysqlconn) {
mail('administrato@myhost.it',Mysql non connesso','Attenzione mysql non risponde alle ore '.date('H:i:s'));
throw new Exception('Errore mysql);
}
}
public static function read($sid){
$sid=mysql_real_escape_string($sid); //quoto per ulteriore sicurezza,potrebbe esserci un attacco di sql-injection sul cookie di sessione
$rs=mysql_query("select dati from sistema.`sessioni` where sid='$sid' limit 1",self::$mysqlconn);
$sessione=mysql_fetch_row($rs);
return $sessione[0];
}
public static function write($sid, $dati_sessione){
$sid=mysql_real_escape_string($sid);
$dati_sessione=mysql_real_escape_string($dati_sessione);
mysql_query("insert into sistema.`sessioni` values( '$sid' , '$dati_sessione' , unix_timestamp() )
on duplicate key dati='$dati_sessione', time=unix_timestamp() ",self::$mysqlconn);
}
public static function destroy($sid){
$sid=mysql_real_escape_string($sid);
mysql_query("delete from sistema.`sessioni` where sid='$sid' limit 1",self::$mysqlconn);
}
public static function gc($maxlifetime){
mysql_query("delete from sistema.`sessioni` where time<unix_timestamp() - $maxlifetime ",self::$mysqlconn);
}
public static function close(){
mysql_close(self::$mysqlconn);
}
}
?>
Cominciamo con il sottolineare che in questa classe non si antepongono prefissi ai SID, perché avendo una tabella e addirittura uno schema dedicati, non ci sono rischi di collisioni tra i nomi delle chiavi memorizzate; inoltre tutti i parametri, ed in special modo i SID, vengono sempre quotati prima di essere inseriti nelle query, questo perché non bisogna mai dimenticare che il SID (il Session ID) viaggia sui cookie (e nel peggiore dei casi sulle url) e quindi un hacker potrebbe tentare di modificarne il valore per effettuare una SQL-Injection.
Nel metodo open()
, in analogia alla classe memcache_mySessionHandler
si effettua la connessione al servizio, in questo caso però i parametri necessari sono tre: host, utente e password, che memorizzeremo nel session.save_path
seguiti da '/', quindi nel caso di un db posizionato sulla porta non standard 6603 del server 192.128.5.3 a cui accediamo con login:root e password:pwd, il php.ini dovrà presentare session.save_path='192.128.5.3:6603/root/pwd'.
Naturalmente è sempre possibile eseguire la valorizzazione di tale parametro da programma attraverso il comando set_ini('session.save_path','192.128.5.3:6603/root/pwd')
o implementando una specifica estensione di classe che integri i parametri nell'overriding del metodo open()
analogamente a quando realizzato nella local_memcache_mySessionHandler
.
Ogni volta che la sessione viene salvata nella relativa tabella, la colonna time viene valorizzata con la funzione MySQL: unix_timestamp
che restituisce il numero di secondi dalla mezzanotte del 1/1/1970 al momento in cui è stata eseguita. Questo torna utile nella fase di implementazione del metodo gc()
nel quale vengono eliminate tutte le tuple in cui la colonna time si presenta antecedente allo unix_timestamp()
decrementato del numero di secondi di durata massima della sessione.
Attenzione, nel metodo write()
, per salvare la sessione è stato usata un insert con clausola on duplicate
c che ci permette di operare un'update di dati e time in caso di dati già presenti nel DB in modo performante ed "atomico".
La sicurezza nell'handler MySQL
Dal punto di vista della sicurezza, l'architettura della classe precedente è sufficientemente flessibile da permettere di dedicare alla connessione usata per le sessioni un'utenza DB con diritti di modifica/lettura esclusivamente sullo schema 'sistema' sul quale assumiamo non possano operare gli altri account DB, ed il tutto controllato da php.ini p>
Resta però un problema: se il server propone servizi di hosting o ci sono fragilità di code-injection nelle nostre applicazioni un hacker potrebbe leggere i parametri di configurazione ed usarli per leggere le sessioni di tutti gli utenti e realizzare così un session hijacking.
A questo si rimedierebbe formalmente assemblando alcune soluzioni già citate in questo articolo ossia realizzare delle estensioni di mysql_mySessionHandler
con i parametri di connessione integrati nel metodo open()
e posizionati in cartelle protette e fuori dalla web root caricate dinamicamente tramite la direttiva auto_prepend_file
ma avendo l'accortezza di dichiararla FINAL
per evitare rischi di ulteriore estensione da parte dell'hacker che potrebbe introdurre metodi in grado di compiere il lavoro sporco.
Prendiamo ad esempio questa classe:
<?
FINAL class embedded_mysql_mySessionHandler extends mysql_mySessionHandler{
public static function open($ignora1,$ignora2) {
self::$mysqlconn=mysql_pconnect('192.128.5.3:6603', 'root', 'pwd');
if(!self::$mysqlconn) {
mail('administrato@myhost.it', 'Mysql non connesso','Attenzione mysql non risponde alle ore '.date('H:i:s'));
throw new Exception('Errore mysql');
}
}
}
?>
Il codice sottostente per effetto della clausola final andrà sempre in errore ma sarebbe stata sufficiente non averla inserita perché venisse eseguito.
<?
//Estende la classe caricata implicitamente
class hack_embedded_mysql_mySessionHandler extends embedded_mysql_mySessionHandler {
public static function hacked() {
$rs=mysql_query("select * from sistema.sessioni",self::$mysqlconn);
while ($sessione=mysql_fetch_row($rs)) print_r($sessione);
}
}
session_start();//lascia che la classe embedded_mysql_mySessionHandler effettui il collegamento al DB
hack_embedded_mysql_mySessionHandler::hacked();//recupera e mostra tutte le sessioni
?>
A questo punto si potrà obbiettare che in effetti per scrivere quella classe occorrerebbe conoscere troppe cose:
- Il nome della classe da estendere; ma si può ricavare con il comando
get_declared_classes().
- Il nome dell' attributo protetto con il link alla connessione; ma si ottiene con queste due righe:
<?
//Estende la classe caricata implicitamente
class hack_embedded_mysql_mySessionHandler extends embedded_mysql_mySessionHandler {
public static function hacked() {
$rs=mysql_query("select * from sistema.sessioni",self::$mysqlconn);
while ($sessione=mysql_fetch_row($rs)) print_r($sessione);
}
}
session_start();//lascia che la classe embedded_mysql_mySessionHandler effettui il collegamento al DB
hack_embedded_mysql_mySessionHandler::hacked();//recupera e mostra tutte le sessioni
?>
- Il nome della tabelle e lo schema per scrivere la query; che sono ricavabili semplicemente interrogando l'information schema.
Il vero punto da obbiettare, che è poi il motivo per cui ho definito la classe embedded_mysql_mySessionHandler
un rimedio "formale", sta nel fatto che non si tiene conto della "simpatica" caratteristica del comando mysql_query()
di essere eseguibile anche senza parametro relativo alla risorsa quindi, dopo aver caricato l'handler, questo codice svela tutte le sessioni del nostro DB senza bisogno di estendere alcuna classe.
<?
session_start();
$rs=mysql_query("select * from sistema.sessioni");
while ($sessione=mysql_fetch_row($rs)) print_r($sessione);
?>
Una soluzione potrebbe essere quella di sostituire la classe embedded_mysql_mySessionHandler con questa:
<?
FINAL class embedded_mysql_mySessionHandler extends mysql_mySessionHandler{
//nuovo metodo static che esegue la connessione a DB
private static function connect() {
self::$mysqlconn=mysql_pconnect('192.128.5.3:6603', 'root', 'pwd');
if(!self::$mysqlconn) {
mail('administrato@myhost.it','Mysql non connesso','Attenzione mysql non risponde alle ore '.date('H:i:s'));
throw new Exception('Errore mysql');
}
}
public static function open($path,$name) {} //non svolge più alcun ruolo
public static function close() {} // non svolge più alcun ruolo
public static function gc($maxlifetime){
self::connect(); //connette DB
parent::gc($maxlifetime); //esegue funzioni classe madre
parent::close(); //esegue chiusura DB da classe madre
}
public static function read($sid){
self::connect();
$read=parent::read($sid);
parent::close();
return $read;
}
public static function write($sid, $dati_sessione){
self::connect();
parent::write($sid, $dati_sessione);
parent::close(); }
public static function destroy($sid){
self::connect();
parent::destroy($sid);
parent::close();
}
}
?>
In pratica la connessione al DB viene aperta ed immediatamente richiusa ad ogni operazione dell'handler, il che non è decisamente performante, seppure si stiano usando connessioni persistenti.
L'ultima soluzione proposta prevede la cifratura e decifratura dei dati di sessione durante l'operazione di open()
e write()
:
<?
FINAL class embedded_mysql_mySessionHandler extends mysql_mySessionHandler{
public static function open($ignora1,$ignora2) {
self::$mysqlconn=mysql_pconnect('192.128.5.3:6603', 'root', 'pwd');
if(!self::$mysqlconn) {
mail('administrato@myhost.it', 'Mysql non connesso','Attenzione mysql non risponde alle ore '.date('H:i:s'));
throw new Exception('Errore mysql');
}
}
//nuovo metodo che effettua cifratura basata su algoritmo RIJNDAEL 256, uno dei miglio dal punto di vista prestazionale e della sicurezza
private static function encript($data){
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND);
return mcrypt_encrypt(MCRYPT_RIJNDAEL_256, "PasswordDiCifraturaSegreta", $data, MCRYPT_MODE_ECB, $iv);
}
// nuovo metodo che effettua decifratura basata su algoritmo RIJNDAEL 256, uno dei miglio dal punto di vista prestazionale e della sicurezza
private static function decript($data){
return mcrypt_decrypt(MCRYPT_RIJNDAEL_256, "PasswordDiCifraturaSegreta", $data, MCRYPT_MODE_ECB);
}
public static function read($sid){
return self::decript(parent::read(self::encript($sid)));
}
public static function write($sid, $dati_sessione){
parent::write(self::encript($sid), self::encript($dati_sessione));
}
public static function destroy($sid){
parent::destroy(self::encript($sid));
}
}
?>
Ovviamente la connessione resta in balia di hacker, ma sia gli ID sia i dati delle sessioni sono cifrati con una password integrata nei metodi privati e non ricavabile tramite reflection in quanto integrata nel codice (come i parametri di connessione al DB del resto), ovviamente anche la cifratura introduce rallentamenti e prevede l'abilitazione di un'apposita estensione php_mcript ma il vantaggio è quello di avere una tabella delle sessioni interamente cifrata e quindi resa sicura anche da attacchi effettuati direttamente al DB.
Per chiudere definitivamente la questione si potrebbe riscrivere il tutto tramite la libreria mysqli_* che richiede il collegamento al DB come parametro obbligatorio di tutti i comandi, sarebbe un ulteriore vincolo al livello di estensioni da dover abilitare mel PHP ma si avrebbe la certezza di aver fatto il massimo per garantire la sicurezza delle nostre sessioni.
Nota sui lock
Tutte le implementazioni presentate non prevedono lock sulle sessioni, questi possono essere introdotti con relativa semplicità nell'handler MySQL attraverso le funzioni get_lock()
e release_lock()
o il select for update se si usa InnoDB , un po' meno semplice nelle altre versioni. Sebbene sarebbe un ottimo esercizio realizzare delle estensioni delle classi presentate con tale funzionalità per esperienza personale consiglio di introdurre la gestione dei lock solo davanti ad effettive necessità nate in casi reali, poiché in generale possono essere più gli svantaggi che i vantaggi ottenuti.