La richiesta di quest'oggi a Mr. Key riguarda un argomento che di certo i più smaliziati ormai conosceranno a menadito, ma che con altrettanta certezza si pone come un delicato problema di fronte a chi lo affronta per le prime volte. Creare una form di login in realtà prende in causa un serie di aspetti che riguardano la sicurezza di un sito web privato che non si limitano alla semplice creazione di una paginetta contenente due caselle di testo ed un pulsante, ma anche tutta una serie di misure che impediscano ad un utente malintenzionato di violare la sicurezza di cui necessitiamo. I problemi che dovremo affrontare rientrano nelle seguenti categorie:
  1. Protezione di una zona privata del sito web
  2. Protezione dei dati contenuti nel database in cui le credenziali sono memorizzate
  3. Protezione delle credenziali nel supporto di persistenza
  4. Protezione delle credenziali durante il loro invio

ASP.NET mette a disposizione tre sistemi per rendere privata una particolare area del nostro sito web. Essi vanno sotto il nome di Windows, Passport e Forms Authentication. La prima basa il suo funzionamento sulla sicurezza fornita dai sistemi operativi derivati da NT. Sostanzialmente questo comporta che l'accesso all'area protetta sia garantito solo a degli utenti che dispongono di un account sul sistema operativo, e quindi richiede che si sia in grado di creare questi account e di fornire ad essi i necessari privilegi sulle cartelle da proteggere. Questo rende inutilizzabile la Windows Authentication ad esempio alla maggior parte di coloro i quali non dispongano di un web server proprio ma utilizzino dei servizi di hosting. Inoltre va detto che fornire le credenziali di un utente del sistema operativo espone il sistema stesso ad attacchi se tale account non è ben configurato. Ritengo che per la maggior parte dei casi questo tipo di autenticazione sia da evitare.

Il secondo metodo, dei tre che ho anticipato, si basa sull'utilizzo di Passport, un servizio di single-sign-on messo a disposizione da Microsoft ma che ultimamente è stato abbandonato pertanto non ritengo utile spendere altre parole su di esso. Molto più interessante invece è l'approfondimento della Forms Authentication che ci consente di implementare un nostro sistema di autenticazione privato. Questo sistema, di gran lunga il più flessibile dei tre è però anche quello che più facilmente esporrà la nostra area protetta all'intrusione di malintenzionati se non useremo le dovute cautele. Occorre innanziutto accertarsi che al framework .NET sia applicata l'ultima service pack (la SP1) per evitare i problemi dovuti al bug della canonicalization. A questo punto si può iniziare a proteggere l'area semplicemente impostando la Forms Authentication nel web.config come segue:


   
        mode="Forms">
                            
name=".ASPXAUTH"
                
loginUrl="/login.aspx" 
                
protection="All" 
                
timeout="30" />
        <
/authentication>        
        
            users="?"/>
        <
/authorization>
   <
/system.web>
<
/configuration>

Questa configurazione si occupa di impostare la autenticazione di tipo Forms, e di definirne i dettagli quali il nome del cookie che che verrà usato per mantenere le informazioni sull'accesso effettuato, il tipo di protezione di questo cookie e il suo timeout. Inoltre nello stesso elemento forms si definisce anche qual'è la pagina nella quale si dovrà effettuare la login. Questo comporta che un tentativo di accesso ad una pagina protetta da un browser che non disponga del cookie suddetto verrà automaticamente rediretto sulla pagina login.aspx.  Infine nella sezione authorization si definisce che gli utenti sconosciuti (questo significa il "?"), non verranno fatti accedere. Per ulteriori chiarimenti su questi parametri e su altri esposti da questa sezione consiglio la lettura della documentazione dell'SDK che è sufficientemente completa. Tuttavia mi corre l'obbligo di segnalare che è sconsigliabile l'uso della sezione forms->credentials che consentirebbe di mettere le credenziali nel web.config. Mi sembra abbastanza ovvio che questo sia un repository di scarsa sicurezza. Così, il punto uno della scaletta che avevo proposto all'inizio ha ottenuto soddisfazione.

Dopo aver composto la webform in login.aspx viene il momento di affrontare l'autenticazione. Posto che questa webform contenga due TextBox, txtUserName e txtPassword quest'ultima impostata con TextMode="Password" per asteriscare l'input, e un Button denominato bEnter dovremmo gestire la verifica delle credenziali su un nostro database. per comodità faremo riferimento ad un database SQL Server o MSDE, ma quando dirò può essere esteso a qualunque altro RDBMS supportato da ADO.NET. E' evidente che per effettuare il controllo dovremmo eseguire una query simile alla seguente:

SELECT COUNT(*) 
   FROM Users 
   
WHERE UserName @userName AND 
         
UserPassword @password;

E' di vitale importanza l'uso dei parameters in questo caso, per sostituire a runtime le variabili @userName e @password perchè una sostituzione di tipo per così dire "casalingo" mediante concatenazione potrebbe portare alla compromissione di tutta la base dati e non solo dell'area protetta. Questa problematica va sotto il nome di SqlInjection e la sua trattazione esula dallo scopo di queste righe, ma basti ricordare che con questo termine si definiscono quegli attacchi che sfruttano l'immissioni di porzioni si codice SQL in alcune form per ottenere accesso a dati che invece non sarebbero consentiti. Un buon articolo (in inglese) sull'argomento lo si può trovare qui. L'uso dei parameters di ADO.NET consente così di soddisfare anche il punto due della scaletta perchè essi si adoperano ad apportare il necessario escape alle stringhe immesse in base al tipo dei parametri. Ecco di seguito il codice dell'evento click del bottone di login:

public void bEnter_Click( object sender, EventArgs e )
{
    
// stringa concatenata per esigenze di spazio
    
string sql = "select count(*) from Users" +
                 " where UserName = @userName and" + 
                 " UserPassword = @password";

    
// creo una connessione
    
using( SqlConnection conn = new SqlConnection( "" ) )
    {
        
// mi connetto al database
        
conn.Open();

        
// creo un comando
        
using( SqlCommand cmd = new SqlCommand( sql, conn ) )
        {
            
// aggiungo un parametro per lo username
            
SqlParameter param1 = cmd.CreateParameter();
            param1.SqlDbType = SqlDbType.VarChar;
            param1.ParameterName = "@userName";
            param1.Value = txtUserName.Text;
            cmd.Parameters.Add( param1 );

            
// aggiungo un parametro per la password
            
SqlParameter param2 = cmd.CreateParameter();
            param2.SqlDbType = SqlDbType.VarBinary;
            param2.ParameterName = "@password";
            param2.Value = CreateHash( txtPassword.Text,txtUserName.Text );
            cmd.Parameters.Add( param2 );

            
// eseguo la query e restituisco come boolean
            
bool result = Convert.ToBoolean( cmd.ExecuteScalar() );

            
// se la login ha buon fine entro nell'applicazione
            
if ( result )
                FormsAuthentication.RedirectFromLoginPage( txtUserName.Text, 
false );                        
        }
    }
}

Innanzitutto bisogna precisare che la concatenazione dell'istruzione SQL è dovuta a problemi di spazio. Comunque nell'esempio si vede come opportunamente per eseguire la query si siano creati i due parametri che poi sono stati aggiunti al comando stesso. I più attenti si saranno sicuramente accorti della presenza del metodo CreateHash() e del fatto che il SqlDbType del parametro della password è un varbinary. Questo serve per soddisfare anche il punto tre, ovvero la protezione delle credenziali. Tale contromisura si rende necessaria per evitare che un utente che riesca ad accedere al database (ad esempio anche un amministratore di sistema), sia in grado di leggere le password nella tabella Users. Perciò si genererà uno hash a partire dalla password e si memorizzerà quest'ultimo. Per chi non lo ha mai sentito nominare, un algoritmo di hashing è in grado di generare una sequenza di caratteri a partire da un'altra (in questo caso la password originale) che varia molto al variare anche minimo della stringa di partenza. Questa nuova stringa rappresenta in modo univoco la password ma non è reversibile ovvero è impossibile risalire alla stringa originaria. Perciò se memorizziamo nel database l'hash della password invece che la password vera e propria potremmo applicare il medesimo algoritmo alla password immessa dall'utente e poi verificare la correttezza comparando i due hash invece che le due password. Questo meccanismo soffre comunque di una lieve insicurezza dovuta al fatto che disponendo di un gran numero di hash generate sarà possibile individuare delle ripetizioni che con poco sforzo potrebbero portare ad individuare la password originale per tentativi. Per ovviare a questo inconveniente è consigliabile applicare un "salt", un granellino di sale. Questo si può ottenere ad esempio prependendo lo username alla password prima di generare l'hash. In questo modo due utenti che abbiano la medesima password avranno comunque un hash ben diverso e perciò sarà più difficile individuare le pericolose ripetizioni. Ecco il codice del metodo CreateHash().

public byte [] CreateHash( string password, string userName )
{
    MD5CryptoServiceProvider provider = 
        
new 
MD5CryptoServiceProvider();

    
return 
        
provider.ComputeHash( Encoding.UTF8.GetBytes( userName + password ) );
}

Con questo ci siamo messi al sicuro anche dal terzo punto. In merito al quarto ed ultimo punto occorre tener presente che il transito delle credenziali sulla rete, durante la post di una form avviene sempre in chiaro. Per questo motivo, ad esempio se navighiamo dietro ad un proxy server, o comunque se qualcuno decide di intercettare il nostro traffico di rete, sarà sempre possibile che lo username e la password vengano scoperte. L'unica protezione per questo tipo di problema è l'adozione di un certificato digitale e del protocollo SSL almeno per la fase di login al sistema. Sotto SSL i dati che vanno dal client al server e viceversa vengono crittati in modo molto sicuro perciò saremmo in grado di dare la massima sicurezza a chi usa la nostra applicazione.

In questo lungo articolo ho tentato di spiegare con semplicità come arrivare a proteggere con cognizione di causa la propria applicazione. E' evidente che quanto detto potrebbe essere soggetto ad ulteriori integrazioni, ma ritengo che per una applicazione di medie esigenze questa soluzione sia più che buona perchè consente di evitare i più comuni errori che tipicamente affliggono buona parte dei sistemi web odierni.

keyword: login asp.net esempio 

powered by IMHO 1.2

tags: - categories:

Commenti (3) -

# | radicalmente | 29.06.2005 - 01.48

Anche oggi mi è arrivata una sequenza di keyword interessante
per un articoletto nella categoria Ask...

# | Di .NET e di altre amenita' | 29.06.2005 - 01.49

# | rocco barocco | 28.03.2006 - 05.18

L'autenticazione Windows, Forms o passport che sia ha i suoi limiti nel fatto che presuppongono comunque l'utilizzo di un server dedicato, nel caso di hosting multihomed, visto che i tre tipi di autenticazione si basano: primo sul fatto che debbano essere applicazioni aspnet (ciò significa che devono essere perlemeno all'interno di una directory virtuale di IIS), secondo che occorre modificare il file web.config adeguatamente, ma il piu' delle volte il web.config deve essere scitto in questo modo:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    
  <system.web>  
    <customErrors mode="off" />  
  
</system.web>

</configuration>
per cui viene meno il concetto stesso di autenticazione messa a punto da ASPNET, dovendo ricorrere al vecchio metodo (vedi ASP o PHP) di aprire una session durante il login o di inviare il caro vecchio cookie e di inserire un controllo all'inizio della pagina sull'esistenza dei parametri di session o del cookie.
Ciao

Aggiungi Commento