Indice dei contenuti
Avere a che fare con i Cookie è una esigenza tipica di qualsiasi programmatore web. In questo articolo, dopo una breve introduzione volta a spiegare il funzionamento dei Cookie in una tipica web application, presenteremo alcune classi helper che consentono di implementare in modo piuttosto semplice le principali attività necessarie per gestire i Cookie all'interno di un qualsiasi progetto ASP.NET Web Forms, MVC e Core: ci riferiamo alle funzionalità di lettura, scrittura, modifica, eliminazione, gestione dei cookie contenenti coppie di chiavi-valori (key-value pairs), ma anche alla gestione di caratteristiche come la data di scadenza, le restrizioni di dominio, la modalità HttpOnly, il flag Secure, e così via. Nell'ultimo paragrafo ci occuperemo di come configurare le medesime impostazioni e caratteristiche a livello globale, ovvero relativamente a tutti i cookie creati dalla nostra applicazione web, utilizzando l'apposito file web.config.
Introduzione
I Cookie svolgono una serie di funzioni importantissime nella maggior parte delle applicazioni web. L'utilizzo più frequente è senz'altro quello legato all'autenticazione automatica (o semi-automatica) degli utenti: i cookie di autenticazione vengono creati dal server sul computer dell'utente al momento del login e contengono i dati necessari per determinare, nel corso delle visite successive, la presenza delle informazioni necessarie a effettuare nuovamente l'accesso. Questi dati, a seconda dei casi, possono contenere informazioni più o meno delicate, come un hash di username e password, un token di autenticazione e così via. Per questo motivo è estremamente importante proteggerli da accessi non autorizzati seguendo una serie di tecniche di sicurezza standard, quali ad esempio:
- data encryption, ovvero la cifratura dei dati personali e dei valori potenzialmente oggetto di attacco (username, password)
- hashing di qualsivoglia password o chiave di sicurezza che non sia fondamentale trasmettere in chiaro.
- limitazione alla lettura/scrittura del cookie da parte di un determinato dominio.
- impostare la data di scadenza (expiration date), così da impedire che il cookie resti attivo a tempo indefinito sul browser.
- limitazione all'accesso da parte del browser attraverso la proprietà HttpOnly che, se abilitata, rende il cookie invisibile a JavaScript e altri linguaggi client-side presenti nella pagina.
- limitazione alla trasmissione alle sole connessioni HTTPS su canale sicuro SSL/TLS attraverso il flag Secure che, se abilitato, rende il cookie impossibile da leggere a eventuali eavesdropper che si frappongano alla comunicazione tra client e server e quindi garantisce contro gli attacchi di tipo man in the middle.
Creare un Cookie
Il seguente metodo statico, scritto in linguaggio C#, si occupa della creazione di un cookie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/// <summary> /// Stores a value in a user Cookie, creating it if it doesn't exists yet. /// </summary> /// <param name="cookieName">Cookie name</param> /// <param name="cookieDomain">Cookie domain (or NULL to use default domain value)</param> /// <param name="keyName">Cookie key name (if the cookie is a keyvalue pair): if NULL or EMPTY, the cookie will be treated as a single variable.</param> /// <param name="value">Value to store into the cookie</param> /// <param name="expirationDate">Expiration Date (set it to NULL to leave default expiration date)</param> /// <param name="httpOnly">set it to TRUE to enable HttpOnly, FALSE otherwise (default: false)</param> /// <param name="sameSite">set it to 'None', 'Lax', 'Strict' or '(-1)' to not add it (default: '(-1)').</param> /// <param name="secure">set it to TRUE to enable Secure (HTTPS only), FALSE otherwise</param> public static void StoreInCookie( string cookieName, string cookieDomain, string keyName, string value, DateTime? expirationDate, bool httpOnly = false, SameSiteMode sameSite = (SameSiteMode)(-1), bool secure = false) { // NOTE: we have to look first in the response, and then in the request. // This is required when we update multiple keys inside the cookie. HttpCookie cookie = HttpContext.Current.Response.Cookies.AllKeys.Contains(cookieName) ? HttpContext.Current.Response.Cookies[cookieName] : HttpContext.Current.Request.Cookies[cookieName]; if (cookie == null) cookie = new HttpCookie(cookieName); if (!String.IsNullOrEmpty(keyName)) cookie.Values.Set(keyName, value); else cookie.Value = value; if (expirationDate.HasValue) cookie.Expires = expirationDate.Value; if (!String.IsNullOrEmpty(cookieDomain)) cookie.Domain = cookieDomain; if (httpOnly) cookie.HttpOnly = true; cookie.Secure = secure; cookie.SameSite = sameSite; HttpContext.Current.Response.Cookies.Set(cookie); } |
Come si può vedere analizzando i parametri del metodo, è possibile specificare il nome, il dominio, la scadenza e la proprietà HttpOnly, mentre il flag Secure è bene impostarlo a livello di file web.config (come avremo modo di vedere in uno dei paragrafi successivi) così da renderlo abilitato (o disabilitato) per tutti i cookie generati dal sito.
Creare un Cookie con set di chiavi-valori
Ciascun cookie rappresenta, per definizione, una singola coppia chiave-valore: la chiave è il nome del cookie, mentre il valore è una normale stringa di testo. Questo significa che, in condizioni normali, per passare molteplici informazioni - come ad esempio username, password e data di login - avrò bisogno di molteplici cookie. Se questo modo di procedere può sembrare inefficiente, è possibile sfruttare uno dei tanti standard di serializzazione esistenti per fare in modo che un singolo cookie contenga più coppie chiave-valore, ovvero un set di chiavi-valori.
Questa possibilità è implementata nel seguente metodo statico, scritto in linguaggio C#, che può essere utilizzato per creare un singolo cookie contenente un set di chiavi-valori attraverso un Dictionary di stringhe:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <summary> /// Stores multiple values in a Cookie using a key-value dictionary, creating the cookie (and/or the key) if it doesn't exists yet. /// </summary> /// <param name="cookieName">Cookie name</param> /// <param name="cookieDomain">Cookie domain (or NULL to use default domain value)</param> /// <param name="keyName">Cookie key name (if the cookie is a keyvalue pair): if NULL or EMPTY, this method will raise an exception since it's required when inserting multiple values.</param> /// <param name="values">Values to store into the cookie</param> /// <param name="expirationDate">Expiration Date (set it to NULL to leave default expiration date)</param> /// <param name="httpOnly">set it to TRUE to enable HttpOnly, FALSE otherwise (default: false)</param> /// <param name="sameSite">set it to 'None', 'Lax', 'Strict' or '(-1)' to not add it (default: '(-1)').</param> /// <param name="secure">set it to TRUE to enable Secure (HTTPS only), FALSE otherwise</param> public static void StoreInCookie( string cookieName, string cookieDomain, Dictionary<string, string> keyValueDictionary, DateTime? expirationDate, bool httpOnly = false, SameSiteMode sameSite = (SameSiteMode)(-1), bool secure = false) { // NOTE: we have to look first in the response, and then in the request. // This is required when we update multiple keys inside the cookie. HttpCookie cookie = HttpContext.Current.Response.Cookies.AllKeys.Contains(cookieName) ? HttpContext.Current.Response.Cookies[cookieName] : HttpContext.Current.Request.Cookies[cookieName]; if (cookie == null) cookie = new HttpCookie(cookieName); if (keyValueDictionary == null || keyValueDictionary.Count == 0) cookie.Value = null; else foreach (var kvp in keyValueDictionary) cookie.Values.Set(kvp.Key, kvp.Value); if (expirationDate.HasValue) cookie.Expires = expirationDate.Value; if (!String.IsNullOrEmpty(cookieDomain)) cookie.Domain = cookieDomain; if (httpOnly) cookie.HttpOnly = true; cookie.Secure = secure; cookie.SameSite = sameSite; HttpContext.Current.Response.Cookies.Set(cookie); } |
Come si può vedere, il dictionary viene serializzzato all'interno della proprietà Values della classe HttpCookie messa a disposizione da ASP.NET.
Con questa tecnica è possibile memorizzare all'interno del cookie veri e propri oggetti, a patto di serializzare le loro proprietà in stringhe. Al tempo stesso, è bene non esagerare con questo approccio, poiché il processo di serializzazione e deserializzazione del cookie è sicuramente molto meno efficiente rispetto all'utilizzo degli stessi previsto dalle numerose specifiche che si sono succedute per definire il loro funzionamento (dalla US5774670 del 1998 alla RFC6265 attualmente in vigore).
Leggere un Cookie
Il seguente metodo statico, anch'esso in C#, consente di recuperare una stringa corrispondente al valore di un singolo Cookie oppure, opzionalmente, a una singola chiave presente all'interno di un singolo Cookie.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/// <summary> /// Retrieves a single value from Request.Cookies /// </summary> public static string GetFromCookie(string cookieName, string keyName) { HttpCookie cookie = HttpContext.Current.Request.Cookies[cookieName]; if (cookie != null) { string val = (!String.IsNullOrEmpty(keyName)) ? cookie[keyName] : cookie.Value; if (!String.IsNullOrEmpty(val)) return Uri.UnescapeDataString(val); } return null; } |
Ovviamente, la lettura della chiave ha senso solo se quest'ultima è stata "scritta" con il metodo precedente, ovvero rispettando lo standard di serializzazione predefinito di ASP.NET. La lettura del singolo cookie è invece pienamente compatibile a prescindere dalla piattaforma utilizzata.
Eliminare un Cookie
Il seguente metodo statico in C# consente di eliminare un cookie, oppure, opzionalmente, una singola chiave da un cookie esistente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/// <summary> /// Removes a single value from a cookie or the whole cookie (if keyName is null) /// </summary> /// <param name="cookieName">Cookie name to remove (or to remove a KeyValue in)</param> /// <param name="keyName">the name of the key value to remove. If NULL or EMPTY, the whole cookie will be removed.</param> /// <param name="domain">cookie domain (required if you need to delete a .domain.it type of cookie)</param> public static void RemoveCookie(string cookieName, string keyName, string domain) { if (HttpContext.Current.Request.Cookies[cookieName] != null) { HttpCookie cookie = HttpContext.Current.Request.Cookies[cookieName]; // SameSite.None Cookies won't be accepted by Google Chrome and other modern browsers if they're not secure, which would lead in a "non-deletion" bug. // in this specific scenario, we need to avoid emitting the SameSite attribute to ensure that the cookie will be deleted. if (cookie.SameSite == SameSiteMode.None && !cookie.Secure) cookie.SameSite = (SameSiteMode)(-1); if (String.IsNullOrEmpty(keyName)) { cookie.Expires = DateTime.UtcNow.AddYears(-1); if (!String.IsNullOrEmpty(domain)) cookie.Domain = domain; HttpContext.Current.Response.Cookies.Add(cookie); HttpContext.Current.Request.Cookies.Remove(cookieName); } else { cookie.Values.Remove(keyName); if (!String.IsNullOrEmpty(domain)) cookie.Domain = domain; HttpContext.Current.Response.Cookies.Add(cookie); } } } |
Controllo dell'esistenza di un Cookie
L'ultimo metodo che presentiamo si occupa di determinare se un cookie esiste o meno, senza modificarlo in alcun modo.
1 2 3 4 5 6 7 8 9 10 |
/// <summary> /// Checks if a cookie / key exists in the current HttpContext. /// </summary> public static bool CookieExist(string cookieName, string keyName) { HttpCookieCollection cookies = HttpContext.Current.Request.Cookies; return (String.IsNullOrEmpty(keyName)) ? cookies[cookieName] != null : cookies[cookieName] != null && cookies[cookieName][keyName] != null; } |
Configurazione dell'Applicazione Web
Ora che abbiamo visto come è possibile gestire i cookie a livello di codice è il momento di dare un'occhiata al file web.config per capire come possiamo impostare globalmente alcune caratteristiche relative a tutti i Cookie che saranno creati, modificati o altrimenti gestiti dalla nostra applicazione.
Secure Flag (requireSSL)
Se abbiamo configurato il nostro sito per accettare unicamente richieste web provenienti da canali protetti (HTTPS con certificato SSL/TLS) vale senz'altro la pena di impostare il flag Secure come attivo: prima di farlo, però, è opportuno spendere un paio di minuti per comprenderne il funzionamento.
Il flag Secure, se presente, indica al browser di autorizzare la creazione (e nelle versioni più recenti dei browser, anche la lettura) di cookie da parte del nostro sito solo utilizzando una connessione protetta. Il fatto che i browser meno recenti consentano la lettura dei cookie Secure anche attraverso connessioni non protette è indubbiamente un problema, che può essere però fortemente mitigato a livello server consentendo solo connessioni protette e rifiutando (o reindirizzando tramite apposite regole di rewrite/redirect da HTTP a HTTPS) quelle provenienti da canali non sicuri; al tempo stesso, è opportuno sottolineare come il senso dell'utilizzo del flag Secure sia soprattutto quello di difendere i nostri "preziosi" cookie di autenticazione da attacchi di tipo tampering, ovvero quelli che potrebbe effettuare un ipotetico man-in-the-middle (MITM) impadronendosi o alterando i nostri dati di sessione.
Per impostare il flag Secure globalmente, inserire i seguenti parametri di configurazione nel file web.config:
1 2 3 4 5 6 |
<configuration> <system.web> <!-- Force secure connections for all Cookies --> <httpCookies requireSSL="true" /> </system.web> </configuration> |
Nel caso in cui l'applicazione web utilizzi la Forms Authentication, ovvero l'autenticazione utente tramite form, è necessario attivare il flag Secure anche nella configurazione delle suddette form onde evitare che i cookie di autenticazione ereditino le impostazioni (per default non sicure) delle suddette. Per gestire anche questo caso, inserire i seguenti parametri di configurazione nel file web.config:
1 2 3 4 5 6 7 8 |
<configuration> <system.web> <authentication mode="Forms"> <!-- Force secure connections for Forms Authentication --> <forms requireSSL="true" /> </authentication> </system.web> </configuration> |
HttpOnly Flag
Come già spiegato in precedenza, la presenza del flag HttpOnly - per default non attivo - indica al browser di non rendere disponibili i Cookie a tutti gli script client-side, come ad esempio gli script realizzati in JavaScript. Per forzare l'attivazione di questo flag a livello globale, ovvero per tutti i cookie creati dalla nostra applicazione web, è necessario utilizzare l'attributo httpOnly="true" all'interno del file web.config nello stesso elemento già definito in precedenza:
1 2 3 4 5 6 |
<configuration> <system.web> <!-- Prevent client-side scripts from reading Cookies --> <httpCookies httpOnlyCookies="true" /> </system.web> </configuration> |
Restrizioni di dominio
Anche le restrizioni di dominio, delle quali abbiamo già parlato in precedenza, possono essere impostate a livello globale impostando il file web.config nel seguente modo:
1 2 3 4 5 6 |
<configuration> <system.web> <!-- Prevent access to cookies from any external domain --> <httpCookies domain=".mywebsite.com" /> </system.web> </configuration> |
Conclusioni
Per il momento è opportuno fermarci qui: ci auguriamo che questa guida ai Cookies in ASP.NET possa essere utile a tutti gli sviluppatori e amministratori di sistema chiamati a cimentarsi con queste problematiche. Alla prossima, e... felice sviluppo!