Un sito web senza cookie? Si può fare

Immagine di congerdesign da Pixabay

Agli albori della mia carriera di programmatore web mi piaceva fare in modo che i miei siti funzionassero bene anche senza JavaScript e senza cookie. Capitava spesso infatti che alcuni utenti disattivassero entrambi per ragioni di sicurezza. Con l’arrivo della legge sui cookie, la mia premura è tornata attuale. L’informativa obbligatoria su tutti i siti che visitiamo è oltremodo fastidiosa e per qualcuno che non è esperto di questioni legali c’è sempre il rischio di non fare le cose completamente in regola. Vale dunque la pena di chiedersi se installare cookie nel browser dell’utente è davvero necessario.

Cominciamo col dire che, a parte gli strumenti di tracciamento come Google Analytics, l’uso principale dei cookie è quello di mantenere una sessione (nel qual caso si parla di “cookie tecnici”), mentre la funzione di salvare preferenze dell’utente è ormai trasferita quasi del tutto alla sessione stessa e all’archiviazione locale via Javascript. Premesso che si parla già dell’eliminazione completa dei cookie in pochi anni e che esistono già varie tecniche di tracciamento che ne fanno a meno, vorrei concentrarmi su come mantenere una sessione in sicurezza facendo a meno di questo collaudato strumento.

Applicazioni a pagina singola

Le applicazioni a singola pagina sono quei siti web in cui il browser carica inizialmente una sola pagina, il cui contenuto viene poi gestito interamente via JavaScript. Servizi che usiamo quotidianamente sono costruiti in questa maniera. Facebook è un esempio chiaro, ma sono così anche il motore di ricerca di Google e molti altri. Svariati strumenti di sviluppo consentono di creare un sito web a pagina singola, come ad esempio i framework AngularJs e React, ma ci si può arrangiare anche con la coppia Laravel + Livewire e con molto altro.
Il buono di questa tecnica è che, non dovendo caricare più pagine, non dobbiamo preoccuparci di recuperare la sessione di lavoro. Possiamo infatti lavorare alla nostra applicazione esattamente come se si trattasse di un servizio web (web service), disaccoppiando completamente il lato server ed il lato client. Un buon servizio web è senza stato, come prevede il protocollo HTTP, e dovrà occuparsi di verificare le credenziali dell’utente a ogni richiesta. Non ci sarà quindi una sessione, ma solo risorse su cui l’utente svolgerà operazioni attraverso le varie richieste (si veda il concetto di server RESTful).
Tipicamente, avremo un server di autenticazione che si occuperà di creare un token. Quest’ultimo sarà salvato localmente (in una semplice variabile Javascript o nell’archiviazione del browser) ed incluso nell’intestazione (header) di ogni richiesta al servizio web, che lo utilizzerà per identificare l’utente.
Non mi dilungherò né sui concetti di autenticazione ed autorizzazione, né su come implementarli in concreto. Ci sono svariati strumenti e protocolli che si possono utilizzare e la scelta dipende da molti fattori, come il tipo di applicazione da sviluppare (un’app per una banca o per un servizio medico avrà un livello di sicurezza molto più alto dell’accesso all’area commenti di un blog, per esempio).

Applicazioni a pagina multipla

In questo caso ci troviamo di fronte ad un sito web tradizionale, composto da varie pagine, con uno stato salvato nella sessione. Nei miei esempi farò riferimento a PHP come linguaggio lato-server, ma gli stessi concetti valgono qualsiasi linguaggio utilizziamo.
Normalmente la sessione viene mantenuta salvando un ID nei cookie. Il browser invia i cookie con ogni richiesta, quindi il programmatore può semplicemente chiamare session_start ad ogni esecuzione dello script ed i dati salvati saranno lì. Si può fare a meno di salvare l’ID della sessione come cookie, a patto di riuscire ad inviarlo in sicurezza in un’altra maniera.

Parametro GET

Il metodo più semplice per inviare l’ID della sessione senza cookie è quello di aggiungerlo alla query string di ogni richiesta, che quindi apparirà come www.miosito.com/pagina?PHPSESSID=mio-id-sessione
L’ID stesso è recuperabile (ed impostabile) chiamando la funzione session_id. PHP estrarrà l’ID dalla richiesta e tutto funzionerà esattamente come con i cookie attivati. I più attenti avranno già notato uno scenario problematico nell’utilizzo di questa tecnica. Immaginiamo che un utente acceda alla pagina e la trovi molto interessante. A questo punto farà una copia dell’URL e lo invierà ad un amico o, peggio, lo condividerà su un social. Chiunque aprirà il collegamento utilizzerà la stessa sessione del primo utente. Se la sessione contiene solo impostazioni di secondaria importanza non sarà un grosso problema, ma se vi sono contenuti dati personali e/o l’ID dell’utente che permette l’accesso ad aree private, questo è inaccettabile. Potremmo adottare qualche accorgimento, come una verifica su alcuni dati dell’utente (indirizzo IP, user agent ed altri dati) ma nulla di ciò che troviamo in una normale richiesta è veramente univoco: due persone connesse con la stessa versione del browser alla stessa rete potrebbero essere facilmente confuse.

Parametro POST

Per evitare di condividere la sessione insieme a tutti i collegamenti possiamo usare un approccio differente, quello di usare richieste di tipo POST, le stesse dei moduli (form), ed inserire l’ID della sessione come campo nascosto. Normalmente non si può usare POST per un semplice collegamento ad un’altra pagina, ma con CSS possiamo rendere un pulsante d’invio (submit) in tutto e per tutto simile ad un tag <a>. Per comodità, possiamo usare una libreria che offra questa possibilità. Con bootstrap basta aggiungere al pulsante le classi “btn btn-link”. In questa maniera, ogni link condiviso porterà ad una pagina con una sessione pulita. Benché migliore della soluzione con parametro GET, anche questa ha diversi problemi:

  1. in primis, è un utilizzo improprio della richiesta di tipo POST. Non vi è accordo se POST debba essere usato per creare nuove risorse o modificarle, ma per una semplice visualizzazione, è GET il tipo designato. Poco male, comunque. È un buon prezzo per una navigazione in tranquillità, senza cookie e senza fastidiosi popup;
  2. l’utente stesso a cui la sessione è associata rientrando troverà una sessione pulita, senza il suo eventuale accesso al suo profilo, esattamente come se avesse cancellato i cookie con l’approccio classico. Se questo è più che desiderabile per un conto bancario, può essere seccante per qualsiasi altro utilizzo meno critico;
  3. l’ID della sessione compare nel codice sorgente della pagina. Questo significa che se salviamo la pagina come file HTML e lo condividiamo con qualcuno, il destinatario avrà accesso alla nostra sessione con un semplice click su un collegamento, tornando in parte allo stesso problema dei parametri GET.

Parametro POST via JavaScript

Possiamo lasciar correre il problema n.1 del punto precedente perché esclusivamente formale, mentre gli altri due sono più importanti per l’utente medio. Possiamo risolverli con l’utilizzo di JavaScript, che ormai tutti mantengono attivo sul browser, dal momento che è alla base di quasi tutte le piattaforme d’uso comune.
Invece di passare la sessione insieme al codice HTML, creiamo un’API che ne avvii una nuova e restituisca l’ID. Il nostro codice JavaScript utilizzerà quest’API per salvare l’ID nell’archiviazione locale del browser e poi ad ogni click su uno dei collegamenti imposterà l’apposito campo del modulo. Se l’ID è già presente in locale, verrà utilizzato per ripristinare la sessione. In questo modo, a patto di ricevere via AJAX eventuali informazioni del profilo da mostrare nella pagina (status di accesso, nome utente, ecc), l’utente ritroverà la sua sessione senza dover accedere di nuovo ed essendo l’ID aggiunto “al volo” al modulo da inviare, non comparirà nel codice sorgente.
Il codice che segue rappresenta questa soluzione, in una versione essenziale. Per semplicità, tutto si trova in un’unico file, ma voi ricordate di creare sempre file separati per PHP, template HTML, CSS e JavaScript.

<?php

/* Accetta PHPSESSID da parametri GET e POST 
   Se il server non lo permette, occorrerà passare manualmente il valore di PHPSESSID a session_id() */
ini_set('session.use_only_cookies', false);
// Non creare il cookie PHPSESSID
ini_set('session.use_cookies', false);

if (isset($_POST['PHPSESSID']) || isset($_POST['get_session_id'])) {
  session_start();
}

if (isset($_POST['get_session_id'])) {
  echo session_id();
  exit();
}
?>

<!doctype html>
<html lang="it">
  <head>
    <title>Il mio sito senza cookie</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css">
  </head>
  <body>
    <form action="index.php?pagina=link1" method="post">
      <input type="hidden" name="PHPSESSID" value="">
      <input type="submit" class="btn btn-link" value="Link 1">
    </form>
    <form action="index.php?pagina=link2" method="post">
      <input type="hidden" name="PHPSESSID" value="">
      <input type="submit" class="btn btn-link" value="Link 2">
    </form>
    
    <p>
      Valore di session_id: <span><?php echo session_id() ?: 'NON PRESENTE' ?></span><br>
      Sessione salvata nel browser: <span id="session_js"></span><br>
      Se i due valori saranno uguali visitando i vari link, tutto funziona correttamente.
    </p>

    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>
    <script>
      $(document).ready(function () {
        // Cerca l'ID di sessione nell'archiviazione del browser
        let session_id = window.localStorage.getItem('user_session');
        if (session_id === null) {
          // L'ID non è salvato. Ne viene richiesto uno nuovo
          $.post("index.php", {get_session_id: 1})
          .done(function(data) {
            window.localStorage.setItem('user_session', data);
            $('#session_js').html(data);
          })
          .fail(function() {
            alert("Impossibile creare una sessione");
          });
        } else {
          $('#session_js').html(session_id);
        }

        $('.btn-link').click(function (e) {
          // Click su uno dei collegamenti. Aggiungi l'ID di sessione ed invia.
          e.preventDefault();
          const $form = $(this).parent();
          $form.children('input[name="PHPSESSID"]').val(window.localStorage.getItem('user_session'));
          $form.submit();
        });
      });
    </script>
  </body>
</html>

Considerazioni sulla sicurezza

Salvare l’ID della sessione nell’archiviazione del browser non è meno sicuro di usare un cookie. Forse nemmeno di più, considerato che ci sono svariate tecniche di hackeraggio a cui dobbiamo prestare attenzione. L’archiviazione locale usa dei file nel dispositivo dell’utente, esattamente come per i cookie, e ne restringe l’accesso a pagine provenienti dalla stessa origine. Qualsiasi utente collegato alla rete deve prendere tutte le precauzioni possibili per evitare che malware attacchino il dispositivo e ne rubino dati importanti, codici di sessione inclusi.
I cookie avevano fino a qualche anno fa il grosso problema di essere inviati insieme a tutte le richieste al server, comprese quelle non sicure, e quindi di essere leggibili da chiunque riuscisse ad intercettare il traffico sulla rete. I dati salvati a livello locale vengono inviati al server solo quando ce n’è effettivamente bisogno, dando più controllo al programmatore sul loro traffico. Ad ogni modo, con l’uso sempre più massiccio di HTTPS dobbiamo preoccuparci di meno che vengano rubati per strada mentre, come abbiamo visto, è ancora importante evitare che siano esposto quando questo risulta non necessario o addirittura dannoso.

Conclusioni

I cookie sono stati uno strumento importante per mantenere lo stato fra una richiesta e l’altra al server web attraverso un protocollo, HTTP, che di per sé è senza stato. Nella loro semplicità si sono rivelati comunque uno strumento potente e spesso utilizzato per fini non proprio cristallini, come quello di seguire passo-passo l’attività dell’utente al fine di registrarne l’attività, le abitudini ed a volte persino dati sensibili. L’Unione Europea è corsa ai ripari per garantire la nostra riservatezza, ma non si può dire che abbia scelto il modo migliore. Sebbene sia sacrosanto informare gli utenti dell’uso che si vuol fare dei loro dati e chiederne il consenso, ci siamo ritrovati con fastidiosi pop-up ad ogni navigazione, fatto ancora più irritante per chi sceglie di cancellare automaticamente i dati di navigazione all’uscita del browser. Peraltro, non è detto che siti malevoli rispettino davvero le nostre scelte, quindi rimane importante che l’utente presti attenzione a ciò che viene salvato nel proprio dispositivo. Sarebbe stato forse meglio tipizzare i cookie e dare all’utente la maniera di configurare una volta per tutte il comportamento del browser in presenza dell’uno o l’altro tipo e permettendo di creare eccezioni in senso più permissivo o più restrittivo rispetto all’impostazione generale. Si avrebbe avuto un controllo più standardizzato ed una navigazione più agevole, ma sembra che ormai la direzione sia quella di abbandonare del tutto i cookie, anche se, come già accennato, ci sono tecniche di tracciamento che funzionano altrettanto bene ed in modo più subdolo.
Se si giungesse ad un web senza cookie sarebbe auspicabile avere un campo standard per l’ID della sessione nella prossima versione di HTTP, ma come abbiamo visto in quest’articolo è già possibile farne a meno sfruttando ciò che abbiamo già a disposizione.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *