Un framework ASP.NET per le Human Interactive Proof

Stephen Toub
Microsoft Corporation

Settembre 2004

Riassunto: Stephen Toub illustra i concetti associati alle Human Interactive Proof (HIP) e la creazione di un framework che consente di incorporarlo nei siti ASP.NET (26 pagine stampate).

Questo articolo contiene collegamenti a siti e pagine Web in inglese.

Scarica il file di esempio MSDNHip.msi.

Sommario

Introduzione
Test di Turing inverso
Human Interactive Proof
Inserimento di HIP in ASP.NET
HipChallenge
ImageHipChallenge
HipValidator
AudioHipChallenge
Funziona?
Pubblicazioni correlate

Introduzione

Il Web è un posto pericoloso. I pirati informatici sono sempre in agguato e con l'aiuto di armi potenti tentano senza sosta di danneggiare, contaminare, compromettere, chiudere o semplicemente sfruttare l'altrui presenza sul Web. Per contrastarli è possibile ricorrere a patch di protezione, controlli di convalida dell'input, crittografia, esecuzione con privilegi minimi, riduzione della superficie esposta agli attacchi e a una miriade di altre tecniche di codifica protette, come quelle illustrate nell'eccellente lavoro di Michael Howard e David LeBlanc sull'argomento, Writing Secure Code (Microsoft Press, 2003). Ci sono però attacchi sempre nuovi e da nuove angolazioni, per cui è indispensabile che anche le difese si evolvano.

Uno degli strumenti più micidiali dell'arsenale di un pirata informatico è la potenza di elaborazione. Può sembrare banale, ma è vero. La velocità con cui un computer può inviare migliaia e migliaia di richieste a migliaia e migliaia di siti è sconcertante e spesso questa possibilità viene utilizzata per attacchi che a rigore potrebbero non essere considerati neppure tali. Per fare un esempio, Hotmail offre al pubblico account di posta elettronica gratuiti e liberamente accessibili, intesi come un utile servizio agli utenti e il cui gradimento è dimostrato dalle centinaia di milioni di utenti registrati nel sito. Decisamente sgradito per Hotmail è invece il fatto che migliaia di questi account vengano utilizzati da pirati informatici per inviare agli utenti ignari tonnellate di posta indesiderata. Il problema per Hotmail e gli altri siti che si trovano nello stesso frangente è che può essere estremamente difficile, se non impossibile, distinguere il browser di un'anziana signora che apre un account per scambiare corrispondenza con suo nipote da un pirata informatico che con un'applicazione personalizzata tenta di aprire automaticamente più account per inviare a quel nipote posta di tutt'altro genere. Qual è allora la soluzione? In che modo in un sito Web è possibile riconoscere le richieste inoltrate da un essere umano rispetto a quelle generate da un programma mediante uno script?

Test di Turing inverso

Nel 1936 Alan Turing pubblicò l'articolo On Computable Numbers, with an Application to the Entscheidungsproblem, oggi famoso, in cui presentava l'idea di quelle che verranno chiamate "macchine di Turing" (http://mathworld.wolfram.com/TuringMachine.html). Nei primi anni '40, insieme a Gordon Welchman, realizzò una macchina in grado di decifrare il codice Enigma della Luftwaffe. Una persona davvero speciale. Nel 1950 Alan Turing cambiò però radicalmente il mondo dell'intelligenza artificiale proponendo ciò che oggi è noto come test di Turing.

Turing era convinto che un giorno i computer costruiti dall'uomo avrebbero eguagliato le capacità e l'intelligenza umane. Dopo tutto, il cervello umano è una forma di computer, sia pure basata sulla biologia invece che sul silicio. Turing riteneva che non avesse senso domandarsi se i computer sono in grado di pensare, e che il vero test di intelligenza consiste piuttosto nello stabilire se un essere umano è in grado di distinguere un'intelligenza artificiale creata dall'uomo da quella umana. A sostegno della sua tesi propose un "gioco di imitazione": un computer in una stanza, un uomo in un'altra e tra i due un terzo, un "interrogatore" umano, che non sa in quale delle due stanze si trovi l'uno o l'altro. Attraverso un'interfaccia testuale, l'esaminatore rivolge domande a entrambe le stanze e, quando si ritiene soddisfatto, prova a indovinare in quale stanza si trova il computer e in quale l'uomo. Se l'esaminatore si sbaglia per più di metà delle volte, il computer supera il test e deve essere considerato altrettanto intelligente dello sfidante umano.

Il test di Turing è incentrato sul problema della capacità da parte dell'uomo di distinguere un computer da un altro essere umano. Per risolvere il problema che noi ci siamo posti, occorre in un certo senso fare il contrario. È necessario che un computer sia in grado di distinguere un essere umano da un altro computer, un compito ben più complesso. Questo scenario è stato formalmente denominato Reverse Turing Test (RTT), test di Turing inverso, anche se la stessa espressione è stata utilizzata in alcune circostanze per descrivere una situazione simile, ma in cui entrambi i "contendenti" tentano di essere riconosciuti come computer.

In che modo quindi un computer può distinguere un essere umano da un altro computer?

Human Interactive Proof

Nel 2000 Yahoo era alla ricerca di una soluzione al problema. L'ingegnere Udi Manber, allora chief scientist di Yahoo, reclutò il professor Manuel Blum della Carnegie Mellon University e, con la collaborazione di uno degli studenti di Ph.D. di Blum, Luis von Ahn, furono creati i CAPTCHA. I CAPTCHA, o più esattamente "Completely Automated Public Turing Tests to Tell Computers and Humans Apart" (test di Turing pubblici completamente automatizzati per distinguere i computer dagli esseri umani) si basano sullo scenario RTT e sono una specie di Human Interactive Proof (HIP), prove di interazione umana. Consistono nel presentare all'utente un rompicapo che ha la caratteristica di essere facilmente risolvibile da un essere umano ma difficile per un computer. In sostanza, sfruttano le differenze esistenti tra l'intelligenza umana e quella artificiale. I CAPTCHA differiscono dal normale test di Turing in quanto sono completamente automatizzati, nel senso che le domande o i rompicapo sono proposti all'utente da un computer e pertanto devono poter essere generati in modo automatico. Oggi i sistemi basati su CAPTCHA sono largamente diffusi nel Web, dai grandi portali Internet come Yahoo e MSN fino a piccoli siti gestiti da privati.

Nel loro documento, Ahn e Blum proposero diversi tipi di rompicapo CAPTCHA, tra cui il più diffuso è probabilmente quello che consiste nella distorsione di una parola, che l'utente deve riconoscere e digitare. Un altro tipo prevede che all'utente venga chiesto di identificare il soggetto rappresentato in una o più immagini in cui una figura comune, ad esempio una scimmia, appare distorta. La figura 1 e la figura 2 illustrano, rispettivamente, esempi di rompicapo dei due tipi descritti. In entrambi i casi la distorsione è importante. Nel primo caso è necessario impedire che il software OCR riconosca la parola. Nel secondo occorre evitare che un pirata informatico possa catalogare le immagini utilizzate dal server.

Figura 1. Cosa c'è scritto?

Figura 2. Che animale è?

Microsoft Research ha dedicato un notevole impegno alla ricerca su questo tipo di sistemi. Yong Rui e Zicheng Liu, ad esempio, hanno proposto un insieme di linee guida per la progettazione di HIP, in grado di garantire che un rompicapo HIP sia sicuro e utilizzabile. Essi hanno inoltre studiato rompicapo HIP basati sul riconoscimento di volti umani e sull'identificazione di lineamenti. Un rompicapo di questo genere è illustrato nella figura 3. In questo caso gli utenti devono individuare un certo numero di elementi specifici, ad esempio quattro angoli degli occhi e due angoli della bocca. Il documento di Rui e Liu è disponibile all'indirizzo http://www.research.microsoft.com/~yongrui/ps/MMSJ04HIP.pdf.

Figura 3. Identificazione di lineamenti

Questi rompicapo trovano applicazione in diversi scenari. Come già accennato, possono costituire un valido sistema per limitare la creazione automatica di account utente. Utilizzando sistemi di questo tipo, AltaVista ha bloccato più del 95% dei tentativi automatizzati di aggiungere URL al suo motore di ricerca. In uno studio approfondito, Benny Pinkas e Tomas Sander, di Hewlett Packard, spiegano come questi sistemi possano essere utilizzati efficacemente nelle pagine di accesso per evitare attacchi del dizionario in linea (http://www.pinkas.net/PAPERS/pwdweb.pdf). I CAPTCHA sono stati utilizzati con successo per bloccare lo spam sui blog e per evitare voti automatici nei sondaggi in linea. Oggi alcune aziende forniscono applicazioni e plug-in per la prevenzione della posta indesiderata basati su questo tipo di tecnologia. Ad esempio, il server di posta potrebbe obbligare il mittente di un messaggio a risolvere un rompicapo prima di autorizzare l'invio. Con una lieve variazione rispetto al sistema sviluppato in questo articolo, i rompicapo potrebbero persino servire a verificare che le richieste inviate a un servizio Web siano generate da un utente e non da un aggressore automatizzato.

Non esiste alcuna implementazione HIP "preconfezionata" per ASP.NET. Tuttavia, il codice necessario per creare un sistema HIP è sorprendentemente semplice.

Inserimento di HIP in ASP.NET

Per il successo di qualsiasi sistema CAPTCHA è fondamentale che i rompicapo siano validi, eppure quelli che presento in questo articolo, basati sulla distorsione di immagini, sono relativamente semplici da violare per qualcuno che sia determinato a farlo. Va detto, comunque, che tutti i rompicapo sono destinati ad essere prima o poi violati. La ricerca nel campo del riconoscimento progredisce a un ritmo tale che quelli attualmente idonei ai fini delle HIP potrebbero non esserlo nel prossimo futuro e molti di quelli più diffusi sono già stati violati, anche se restano comunque utili per i siti che non rivestono uno straordinario interesse per i pirati informatici. In questo senso il problema che viene effettivamente sottoposto agli utenti è senz'altro importante, ma inizialmente è secondario rispetto alla struttura utilizzata per distribuire ed eseguire il rendering dei rompicapo. Man mano che questi ultimi vengono violati è possibile scriverne di nuovi e integrarli nel framework esistente senza che l'operazione richieda agli sviluppatori un impegno significativo per la rifattorizzazione dei siti protetti. Il presente articolo è pertanto incentrato sulla progettazione di un valido framework per l'implementazione di HIP in ASP.NET.

Scomposto nei suoi elementi essenziali, un sistema HIP si rivela costituito da tre parti principali. La prima è il problema vero e proprio, il rompicapo presentato all'utente. Naturalmente, occorre fornire all'utente un modo per rispondere al quesito, quindi la seconda parte consiste in un metodo di input mediante il quale l'utente può rispondere per fornire la soluzione. Terzo, il sistema deve includere un modo per comunicare all'utente se la risposta che ha fornito è corretta o meno. Scomponendo il sistema in questo modo è più semplice realizzare un framework valido per una soluzione ASP.NET. Il mio sarà basato sul framework di controllo ASP.NET.

Il materiale da scaricare associato a questo articolo include l'implementazione di due rompicapo di esempio, uno visivo e l'altro sonoro. Va sottolineato che nessuno dei due è stato collaudato rispetto allo stato attuale dei sistemi di riconoscimento ottico e audio ed entrambi hanno esclusivamente valore di esempi, come si vedrà più dettagliatamente in seguito. Entrambe le implementazioni sono in effetti controlli ASP.NET personalizzati, derivati da una classe base astratta da me creata e denominata HipChallenge.

HipChallenge è relativa alla prima delle tre parti suddette. Tutti i rompicapo derivano dalla classe HipChallenge e possono essere inseriti in un sistema esistente senza quasi richiedere l'intervento di uno sviluppatore. ASP.NET prevede controlli di convalida che consentono di notificare velocemente agli utenti se l'input che hanno fornito non è valido, per cui la parte del sistema relativa alla convalida e alla comunicazione agli utenti è implementata mediante un controllo di convalida ASP.NET personalizzato, HipValidator. Come tutte le altre convalide ASP.NET(RegularExpressionValidator, RequiredFieldValidator, e così via) anche questa è configurata per interagire con gli altri controlli della pagina. HipValidator interagisce con altri due controlli. In primo luogo, può essere configurato in modo da interagire con qualsiasi controllo della pagina derivato da HipChallenge, convalidando l'input degli utenti a fronte del rompicapo presentato da tale controllo. Inoltre, esso viene configurato per monitorare un controllo di input specificato dallo sviluppatore, che costituisce l'ultima parte del sistema. Può trattarsi di qualsiasi controllo che supporti l'immissione di input testuale da parte degli utenti, da convalidare a fronte del rompicapo presentato.

Una pagina in cui siano implementati CAPTCHA conterrà almeno tre controlli: un controllo derivato da HipChallenge, che presenta il rompicapo all'utente; un controllo di input, ad esempio TextBox, che accetta l'input degli utenti e un HipValidator che coordina i primi due e verifica se l'utente ha risolto correttamente il problema.

HipChallenge

HipChallenge è la classe base per qualsiasi controllo che esegue il rendering dei rompicapo e li presenta agli utenti. A una classe derivata è affidato solo il compito di generare il tipo di output per il rompicapo, non il suo contenuto, e tutto il resto viene gestito mediante HipChallenge o il relativo controllo HipValidator, come si vedrà più avanti. Pertanto, la funzionalità centrale di HipChallenge consiste nella scelta del contenuto (in genere una parola o frase) da utilizzare per il rompicapo, nella memorizzazione delle relative informazioni in un controllo nascosto della pagina e nel fornire un metodo per controllare l'input dell'utente nel postback a fronte dei dati memorizzati nel controllo nascosto. Il tutto viene implementato in tre metodi principali, integrati da diversi helper. Il primo e più semplice dei tre è un override di Control.CreateChildControls mediante il quale viene soltanto creato nella pagina un controllo nascosto in cui in seguito memorizzare i dati.

protected override void CreateChildControls()
{
    _hiddenData = new HtmlInputHidden();
    _hiddenData.EnableViewState = false;
    Controls.Add(_hiddenData); 
    base.CreateChildControls();
}

Il metodo successivo, il più importante per la generazione del rompicapo, è un override di OnPreRender e merita qualche spiegazione. Mediante OnPreRender è possibile scegliere il testo del rompicapo da visualizzare, memorizzare le relative informazioni nel controllo _hiddenData creato in CreateChildControls e quindi delegare la generazione del rompicapo alla classe derivata. Ma il secondo passaggio appena definito presenta un problema. Le informazioni sulla parola selezionata devono essere memorizzate nel client, in modo che alle successive richieste sia possibile convalidare la risposta del client, ma non è possibile limitarsi a memorizzare la parola selezionata come testo normale. Perché? La risposta è che in questo modo sarebbe facilissimo per un'applicazione automatizzata ottenere la parola in questione, analizzando l'HTML fornito ed eseguendo una ricerca. È chiaro che per evitare di inviare la parola all'utente con l'HTML si potrebbero utilizzare risorse dal lato server per archiviare le informazioni per ciascun client, ma l'operazione potrebbe diventare costosa.

Per risolvere la situazione, un approccio possibile consiste nel crittografare la parola selezionata. Le informazioni crittografate potrebbero quindi essere memorizzate invece del testo normale nel campo nascosto creato mediante CreateChildControls. Visto che .NET Framework fornisce lo spazio dei nomi System.Security.Cryptography completo di un'ampia gamma di protocolli di crittografia supportati, aggiungere questo livello di protezione è semplicissimo. Tuttavia, ogni volta che si affida alla crittografia la protezione di un segreto, è indispensabile riflettere sui tipi di attacchi che potrebbero essere sferrati contro il sistema. Come primo esempio, occorre chiedersi: in questo caso la crittografia è davvero utile? Sì e no. Crittografare la parola selezionata per il rompicapo rende estremamente difficile, se non impossibile, identificarla. Ma evita davvero ogni altro attacco? Naturalmente no. Un possibile attacco contro una soluzione CAPTCHA consiste nel formulare una serie di richieste, creando un database dei rompicapo presentati e quindi consentendo a una o più persone di fare tutti i tentativi necessari, fino a risolverli. Dopodiché, l'applicazione automatizzata potrebbe riprendere il suo attacco. Non importa che il testo venga crittografato: il rompicapo potrà comunque essere risolto da un essere umano. Per aggirare il problema è necessario fare in modo che il CAPTCHA debba essere risolto entro un intervallo di tempo ragionevole. A questo scopo, invece di limitarsi a crittografare il testo del rompicapo è possibile includere nel contenuto crittografato una scadenza. Quando un utente presenta la soluzione al server, non solo i dati vengono decrittografati, ma viene anche confrontata l'ora corrente con l'ora di scadenza. Una soluzione a tempo scaduto equivale a una risposta errata.

Il concetto risulterà probabilmente familiare a chiunque abbia utilizzato l'autenticazione basata su form ASP.NET. Nell'autenticazione basata su form, le informazioni relative a un utente autenticato vengono inviate dal client al server, e viceversa, in genere all'interno di cookie. Le informazioni sono crittografate e possono includere una data di scadenza che obbliga l'utente a ripetere l'autenticazione dopo un intervallo di tempo predeterminato. Per fortuna questa funzionalità viene esposta mediante la classe FormsAuthentication, e specificamente mediante i relativi metodi statici Encrypt e Decrypt, ed è possibile utilizzarla senza dover reinventare la ruota. Di seguito sono illustrati plausibili metodi Encrypt e Decrypt.

internal static string Encrypt(string content, DateTime expiration)
{
    FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
        1, HttpContext.Current.Request.UserHostAddress, DateTime.Now,
        expiration, false, content);
    return FormsAuthentication.Encrypt(ticket);
}

internal static string Decrypt(string encryptedContent)
{
    try
    {
        FormsAuthenticationTicket ticket =
            FormsAuthentication.Decrypt(encryptedContent);
        if (!ticket.Expired) return ticket.UserData;
    }
    catch (ArgumentException) { }
    return null;
}

Con il metodo Encrypt viene creato un nuovo FormsAuthenticationTicket che contiene il testo da crittografare e la data e l'ora di scadenza. Il ticket viene quindi crittografato mediante FormsAuthentication.Encrypt e la stringa risultante può essere incorporata direttamente nel campo nascosto inviato al client. Il metodo Decrypt consente di decrittografare il contenuto crittografato, presumibilmente acquisito nel postback dal campo nascosto o da una stringa di query generata da una classe derivata. Se il ticket viene decrittografato correttamente e non è ancora scaduto, viene restituito il contenuto in testo normale.

Fin qui tutto bene. La crittografia consente di inviare il rompicapo al client, eliminando l'onere del mantenimento di risorse dal lato server e limitando anche il periodo di validità del rompicapo. Ma c'è un attacco ben più dannoso che in questo modo non viene impedito. Se il pirata informatico risolvesse manualmente il rompicapo e poi fornisse la soluzione all'applicazione? Da quel momento e fino alla scadenza del rompicapo la soluzione potrebbe essere ripetutamente utilizzata per l'attacco automatizzato. Il periodo di validità potrebbe durare più che secondi o minuti, a seconda della configurazione del controllo, e il tempo potrebbe essere sufficiente a permettere l'invio di una pletora di richieste. Di fatto, per risolvere il problema, sono necessarie risorse dal lato server. È l'unica maniera conosciuta per evitare l'invio multiplo della stessa soluzione. Vi è mai capitato, acquistando qualcosa sul Web, che nella fase finale della procedura un messaggio vi avvertisse di non fare clic due volte sul pulsante di invio, per evitare di inoltrare due volte l'ordine? In genere l'avviso indica che nel sito non è previsto il rilevamento degli ordini già inoltrati. La soluzione adottata da alcuni dei più grandi punti vendita sul Web consiste nell'inviare al cliente un identificativo univoco associato al contenuto del carrello corrente. Al momento dell'inoltro dell'ordine l'identificativo viene memorizzato dal lato server e questo indica che è già stato inviato, per cui eventuali invii ripetuti con lo stesso ID verranno ignorati. La stessa soluzione può essere utilizzata per impedire a un pirata informatico di utilizzare più di una volta lo stesso rompicapo e la relativa soluzione.

Dati i passaggi già descritti, il modo più semplice sarebbe quello di memorizzare il testo crittografato dal lato server oltre a inviarlo al client. Quando viene eseguita l'autenticazione il valore può essere contrassegnato nel server come utilizzato e ogni futura richiesta di autenticazione con lo stesso testo crittografato non andrebbe a buon fine. Ovviamente, se i dati verranno memorizzati dal lato server ci sono poche ragioni per inviare anche al client il testo crittografato, visto che i dati crittografati sono relativamente voluminosi e non c'è alcun motivo di fornire a un eventuale pirata informatico più informazioni del necessario, anche se crittografate. Ad esempio, è possibile che dalla loro lunghezza si possa risalire al numero di caratteri della parola oggetto del rompicapo? Allora, se prevenire l'attacco è importante, come è probabile viste le situazioni in cui normalmente vengono impiegate le HIP, una semplice soluzione è quella che segue, implementata nell'esempio di codice associato.

In OnPreRender seleziono il testo del rompicapo e genero un identificatore univoco. Quindi utilizzo l'identificativo per memorizzare il testo selezionato dal lato server. Il meccanismo di memorizzazione non è rilevante ai fini di questa spiegazione. Qui verrà utilizzata la cache ASP.NET ma, se le HIP dovranno essere distribuite in un ambiente di Web farm, probabilmente occorrerà un tipo di stato condiviso tra i server, come un database SQL Server. Invece di crittografare e memorizzare il testo nel controllo _hiddenData, vi si memorizza l'ID. Da questo ID generato in modo casuale un pirata informatico non ottiene alcuna informazione sul testo selezionato. Il testo e l'ID vengono quindi passati alla classe derivata, perché sia possibile generare il rompicapo.

protected sealed override void OnPreRender(EventArgs e)
{
    string content = ChooseWord();
    Guid id = Guid.NewGuid();

    SetChallengeText(id, content, DateTime.Now.AddSeconds(Expiration));
    _hiddenData.Value = id.ToString("N");
    RenderChallenge(id, content);

    base.OnPreRender(e);
}

SetChallengeText consente semplicemente di memorizzare il contenuto della cache ASP.NET utilizzando l'ID come chiave. Si noti che anche in questo caso è impiegato il concetto di scadenza, rimuovendo il testo del rompicapo dalla cache una volta trascorso l'intervallo di tempo specificato. La sua controparte, GetChallengeText, acquisisce un ID e restituisce il problema associato, se disponibile.

L'ultimo metodo importante di HipChallenge è Authenticate.

internal bool Authenticate(string userData)
{
    if (_authenticated == true) return _authenticated;

    if (userData != null && userData.Length > 0 &&
        _hiddenData.Value != null && _hiddenData.Value.Length > 0)
    {
        try
        {
            Guid id = new Guid(_hiddenData.Value);
            string text = GetChallengeText(id);
            if (text != null && string.Compare(userData, text, true) == 0)
            {
                _authenticated = true;
                SetChallengeText(id, null, DateTime.MinValue);
                return true;
            }
        }
        catch(FormatException){}    
    }
    return false;
}

Il metodo viene chiamato durante la convalida dell'input dell'utente per determinare se la parola immessa dall'utente corrisponde a quella utilizzata per generare il rompicapo. L'ID del rompicapo viene recuperato dal campo nascosto e passato a GetChallengeText per richiamare il testo del rompicapo proposto all'utente. Se il testo viene trovato e corrisponde alla soluzione fornita dall'utente, l'autenticazione ha esito positivo. Per evitare che la stessa soluzione venga utilizzata più volte per lo stesso ID, dopo l'autenticazione l'ID e il testo ad esso associato vengono rimossi dalla cache. È chiaro che in questo modo il metodo Authenticate non può funzionare correttamente due volte nella stessa richiesta. Per risolvere il problema, il risultato di Authenticate viene memorizzato nella variabile membro privata authenticated, il cui valore inizialmente è false. Una volta che il corretto userData è stato fornito al metodo Authenticate, ogni altra chiamata ad Authenticate nel corso della stessa richiesta HTTP restituirà true. Poiché _authenticated non è un valore statico, per le future richieste HTTP (che restituiranno una nuova istanza di HipChallenge) sarà necessario ripetere l'autenticazione.

HipChallenge espone anche alcuni altri metodi e proprietà. La proprietà Expiration consente agli sviluppatori di configurare l'intervallo di validità del rompicapo, espresso in secondi (l'impostazione predefinita è 120, o due minuti). La proprietà Words espone un insieme StringCollection che conterrà le parole da cui verranno generati i rompicapo. In alternativa, è possibile eseguire l'override del metodo ChooseWord con un controllo derivato, al fine di personalizzare ulteriormente le modalità di selezione della parola da utilizzare per il rompicapo mediante la classe base. HipChallenge implementa anche alcuni metodi protetti per la generazione casuale di numeri, che servono a richiamare numeri interi e doppi casuali. Tutti questi metodi sono wrapper della classe RandomNumbers, che a sua volta contiene System.Security.Cryptography.RNGCryptoServiceProvider, che fornisce metodi Next e NextDouble simili a quelli esposti da System.Random. Come si può evincere dallo spazio dei nomi, RNGCryptoServiceProvider è un generatore di numeri pseudo-casuali con crittografia sicura, mentre Random non lo è.

ImageHipChallenge

Il controllo ImageHipChallenge consente di presentare all'utente un rompicapo visivo. Nella sua implementazione corrente, si tratta semplicemente di testo distorto su uno sfondo sfumato. Il controllo deriva da HipChallenge e viene dichiarato come segue:

[ToolboxBitmap(typeof(ImageHipChallenge), "msdn.bmp")]
[ToolboxData("<{0}:ImageHipChallenge Runat=\"server\"" +
    "Height=\"100px\" Width=\"300px\" />")]
public class ImageHipChallenge : HipChallenge
{
   ...
}

ToolboxBitmapAttribute mi consente di indicare a Visual Studio .NET quale immagine desidero utilizzare nella casella degli strumenti per il controllo ("msdn.bmp", che viene compilato nell'assembly come risorsa incorporata), mentre ToolboxDataAttribute consente di stabilire quale tag generare per il controllo quando viene inizialmente aggiunto a una pagina.

Come accennato, quando viene eseguito il rendering in una pagina è necessario generare mediante ImageHipChallenge un collegamento immagine a ImageHipChallenge.aspx (o a qualsiasi URL endpoint sia stato configurato utilizzando la proprietà RenderUrl del controllo). In questo passaggio intervengono due metodi. Innanzitutto, il controllo esegue l'override di Control.CreateChildControls per aggiungere un controllo Image che consentirà il rendering del tag img quando verrà chiamato il metodo Render del controllo. Secondo, il controllo esegue l'override di HipChallenge.RenderChallenge per configurare correttamente la proprietà ImageUrl del controllo Image creato in CreateChildControls.

protected sealed override void CreateChildControls()
{
    base.CreateChildControls();

    // Make sure that the size of this control has been properly defined.
    if (this.Width.IsEmpty || this.Width.Type != UnitType.Pixel ||
        this.Height.IsEmpty || this.Height.Type != UnitType.Pixel)
    {
        throw new InvalidOperationException(
            "Must specify size of control in pixels.");
    }

    // Create and configure the dynamic image.
    _image = new System.Web.UI.WebControls.Image();
    _image.BorderColor = this.BorderColor;
    _image.BorderStyle = this.BorderStyle;
    _image.BorderWidth = this.BorderWidth;
    _image.ToolTip = this.ToolTip;
    _image.EnableViewState = false;
    Controls.Add(_image);
}

protected sealed override void RenderChallenge(Guid id, string content)
{
    // Generate the link to the image generation handler
    _image.Width = this.Width;
    _image.Height = this.Height;
    _image.ImageUrl = _renderUrl + "?" + 
        WIDTH_KEY + "=" + (int)Width.Value + "&" + 
        HEIGHT_KEY + "=" + (int)Height.Value + "&" +
        ID_KEY + "=" + id.ToString("N");
}

Il controllo base HipChallenge passa al metodo RenderChallenge sia la parola in testo normale che l'ID del rompicapo. ImageHipChallenge utilizza soltanto quest'ultimo, a causa del meccanismo di generazione dell'immagine ritardato, ma un'altra implementazione potrebbe utilizzare la prima o utilizzarli entrambi. I valori di larghezza e altezza vengono accodati all'URL RenderUrl insieme all'ID del rompicapo. L'URL viene quindi impostato come URL del controllo <B>Image</B> e questo è tutto ciò che occorre per la richiesta.

Quando il browser riceve il rendering della pagina, trova un tag img con un attributo src che punta di nuovo a ImageHipChallenge.aspx. Per questo è necessario che la mia soluzione gestisca le richieste per questo endpoint. A questo scopo ho creato ImageHipChallengeHandler, un IHttpHandler in grado di generare immagini CAPTCHA sulla base dei parametri di larghezza, altezza e ID forniti nella stringa di query. Per configurare l'handler è sufficiente che lo sviluppatore comunichi ad ASP.NET che tutte le richieste per l'endpoint specificato dovranno essere gestite da un'istanza di ImageHipChallengeHandler. Per farlo, lo sviluppatore può modificare Web.config includendovi quanto segue:

<httpHandlers>
    <add verb="*" path="ImageHipChallenge.aspx"
        type="Msdn.Web.UI.WebControls.ImageHipChallengeHandler, Hip"/>
</httpHandlers>

Ciò fatto, tutte le richieste per ImageHipChallenge.aspx saranno instradate a un'istanza di ImageHipChallengeHandler e gestite dal relativo metodo ProcessRequest, illustrato di seguito:

public void ProcessRequest(HttpContext context)
{
    // Retrieve query parameters and the challenge text
    NameValueCollection queryString = context.Request.QueryString;
    int width = 
        Convert.ToInt32(queryString[ImageHipChallenge.WIDTH_KEY]);
    if (width <= 0 || width > MAX_IMAGE_HEIGHT) throw new
        ArgumentOutOfRangeException(ImageHipChallenge.WIDTH_KEY);
    int height =
        Convert.ToInt32(queryString[ImageHipChallenge.HEIGHT_KEY]); 
    if (height <= 0 || height > MAX_IMAGE_HEIGHT) throw new
        ArgumentOutOfRangeException(ImageHipChallenge.HEIGHT_KEY);
    string text = HipChallenge.GetChallengeText(
        new Guid(queryString[ImageHipChallenge.ID_KEY]));

    if (text != null)
    {
        // We successfully retrieved the information, so generate 
        // the image and send it to the client.
        HttpResponse resp = context.Response;
        resp.Clear();
        resp.ContentType = "img/jpeg";
        using(Bitmap bmp = GenerateImage(
            text, new Size(width, height)))
        {
            bmp.Save(resp.OutputStream, ImageFormat.Jpeg);
        }
    }
} 

Alla ricezione della richiesta, il metodo ottiene la larghezza, l'altezza e l'ID del rompicapo dalla stringa di query. L'ID viene quindi utilizzato per recuperare il testo del rompicapo, operazione che avrà esito positivo solo se l'ID è valido e se il contenuto non è scaduto nella cache. Supponendo che il testo venga recuperato, il valore corrente di HttpResponse viene eliminato e il relativo ContentType viene impostato su "img/jpeg", per informare il browser che il contenuto inviato è un'immagine in formato JPEG. Quindi una nuova immagine viene generata in modo dinamico e salvata nell'OutputStream di HttpResponse per essere inviata al client. ContentType e ImageFormat in Image.Save non sono importanti finché entrambi fanno riferimento allo stesso formato di file. Pertanto, invece di "img/jpeg" e ImageFormat.Jpeg, avrei potuto utilizzare "img/gif" e ImageFormat.Gif.

RNGCryptoServiceProvider si utilizza per rendere casuale la scelta delle immagini generate. Per prima cosa viene creata una nuova Bitmap con la larghezza e l'altezza specificate e intorno ad essa viene creata una superficie Graphics. Due colori a 24 bit vengono generati in modo casuale e utilizzati come colori endpoint per un LinearGradientBrush con cui vengono riempite le immagini. Per il pennello viene configurata anche una sfumatura con angolazione casuale, da 0 a 360 gradi. Una volta disegnato lo sfondo, si sceglie una FontFamily a caso tra quelle disponibili. Avrei potuto selezionarne una a caso tra quelle installate nel mio sistema utilizzando FontFamilies.Families, ma ho preferito codificare internamente un breve elenco di famiglie di caratteri per semplificare l'implementazione, ma anche per evitare che la scelta casuale potesse ricadere su un tipo di carattere composto da simboli, che avrebbe reso il testo indecifrabile per l'utente. In base allo spazio disponibile si sceglie una dimensione per il carattere, quindi il testo viene tracciato al centro dell'immagine mediante un altro LinearGradientBrush casuale come quello utilizzato per lo sfondo. Quando il testo è stato disegnato si aggiunge la distorsione all'immagine, spostandone i pixel con un semplice effetto onda:

for (int y = 0; y < height; y++)
{
    for (int x = 0; x < width; x++)
    {
        int newX = (int)(x + (distortion * Math.Sin(Math.PI * y / 64.0)));
        int newY = (int)(y + (distortion * Math.Cos(Math.PI * x / 64.0)));
        if (newX < 0 || newX >= width) newX = 0;
        if (newY < 0 || newY >= height) newY = 0;
        b.SetPixel(x, y, copy.GetPixel(newX, newY));
    }
}

Anche il livello di distorsione viene scelto in modo casuale. Grazie alla combinazione di tanti elementi casuali, la stessa parola avrà un aspetto diverso ogni volta che ne verrà eseguito il rendering. Nella figura 4 sono riportati due diversi rendering della parola "word".

Figura 4. Due rendering casuali di "word" eseguiti mediante ImageHipChallengeHandler

La figura 5 mostra invece due rendering della parola "excel".

Figura 5. Due rendering casuali di "excel" eseguiti mediante ImageHipChallengeHandler

HipValidator

HipValidator è il più semplice dei controlli della mia soluzione. Deriva da BaseValidator, con l'override di un metodo della classe base, EvaluateIsValid, e l'aggiunta di una proprietà aggiuntiva che consente all'utente di specificare a quale HipChallenge viene associato il sistema di convalida. Si noti che il controllo eredita ControlToValidate da BaseValidator, per cui è possibile associarlo a un controllo di input.

[TypeConverter(typeof(HipChallengeControlConverter))]
[Category("Behavior")]
public string HipChallenge
{
    get { return _hipChallenge; }
    set { _hipChallenge = value; }
}

private HipChallenge AssociatedChallenge
{
    get
    {
        if (HipChallenge == null || HipChallenge.Trim().Length == 0) 
            throw new InvalidOperationException(
                "No challenge control specified.");
        HipChallenge hip = 
            NamingContainer.FindControl(HipChallenge) as HipChallenge;
        if (hip == null) throw new InvalidOperationException(
            "Could not find challenge control.");
        return hip;
    }
}

protected override bool EvaluateIsValid()
{
    // Get the validated control and its value. If we can get a value, 
    // see if it authenticates with the associated HipChallenge.
    string controlName = base.ControlToValidate;
    if (controlName != null)
    {
        string controlValue = base.GetControlValidationValue(controlName);
        if (controlValue != null && 
          ((controlValue = controlValue.Trim()).Length > 0))
        {
            return AssociatedChallenge.Authenticate(controlValue);
        }
    }
    return false;
}

EvaluateIsValid semplicemente richiama il ControlToValidate dalla classe base e acquisisce il relativo valore di convalida, ossia il valore della proprietà specificata mediante l'attributo ValidationProperty allegato al controllo, che nel caso della TextBox è la proprietà Text. Il valore viene quindi passato al metodo associato Authenticate del controllo HipChallenge, che restituisce il risultato.

L'unica altra cosa interessante da notare è l'attributo TypeConverter applicato alla proprietà HipChallenge. I TypeConverters hanno due utilizzi correlati. Il primo consiste nel convertire i valori da un tipo all'altro in fase di esecuzione, ad esempio convertendo un System.Drawing.Point in un valore di stringa o da un valore di stringa. Il secondo è semplificare la configurazione della proprietà durante la progettazione. System.ComponentModel.EnumConverter, ad esempio, viene utilizzato automaticamente per qualsiasi proprietà con tipo Enum mostrata in una PropertyGrid. Questo consente all'utente di selezionare il valore dell'enumerazione da un elenco a discesa nella griglia. Per assistere lo sviluppatore nella fase di progettazione con HipValidator, ho creato una speciale classe derivata da TypeConverter, HipChallengeControlConverter, che semplifica la selezione di un'istanza esistente derivata da HipChallenge nella pagina. Invece di digitare manualmente l'ID del controllo nella casella della PropertyGrid, lo sviluppatore può semplicemente selezionarlo da un elenco a discesa in cui sono riportati gli ID di tutti i controlli della pagina derivati da HipChallenge. L'implementazione di un TypeConverter personalizzato a questo scopo richiede poche righe di codice.

private class HipChallengeControlConverter : ValidatedControlConverter
{
    private object[] GetControls(IContainer container)
    {
        ArrayList list = new ArrayList();
        foreach(IComponent comp in container.Components)
        {
            HipChallenge hip = comp as HipChallenge;
            if (hip != null)
            {
                if (hip.ID != null && hip.ID.Trim().Length > 0)
                {
                    list.Add(hip.ID);
                }
            }
        }
        list.Sort(Comparer.Default);
        return list.ToArray();
    }

    public override StandardValuesCollection GetStandardValues(
        ITypeDescriptorContext context)
    {
        if (context == null || context.Container == null) return null;
        object [] controls = GetControls(context.Container);
        if (controls != null) 
            return new StandardValuesCollection(controls);
        return null;
    }
}

Esso deriva da ValidatedControlConverter, il TypeConverter utilizzato per la proprietà ControlToValidate, ed esegue l'override del metodo GetStandardValues. Questo metodo deve restituire un insieme StandardValuesCollection contenente gli ID di stringa da visualizzare nell'elenco a discesa. Quindi non devo far altro che scorrere in ciclo le istanze di IComponent dell'insieme context.Container.Components per individuare i controlli HipChallenge e inserire i relativi ID nell'insieme StandardValuesCollection. Va rilevato che in ASP.NET 2.0 lo scenario è semplificato dall'esistenza della classe ControlIDConverter. Derivando da ControlIDConverter invece che da ValidatedControlConverter, la mia implementazione di HipChallengeControlConverter sarà la seguente:

private class HipChallengeControlConverter : ControlIDConverter
{
    protected override bool FilterControl(Control control)
    {
        return c is HipChallenge;
    }
}

Questa soluzione è stata scritta per ASP.NET 1.1, ma può essere utilizzata anche in ASP.NET 2.0 senza alcuna modifica. Nella figura 6 sono illustrati i controlli ImageHipChallenge e HipValidator incorporati nel Personal Web Site Starter Kit incluso in Visual Web Developer 2005 Express Edition Beta.

Figura 6. Login modificato per incorporare ImageHipChallenge e HipValidator

Se la soluzione viene eseguita in ASP.NET 2.0 i controlli sviluppati in questa sede verranno automaticamente potenziati per supportare la nuova funzionalità di ASP.NET 2.0 che migliora la soluzione. Ad esempio, un problema con i controlli di convalida in ASP.NET 1.x è che il loro ambito è a livello di pagina. Ciò significa che per qualsiasi postback avviato da un controllo della pagina verrà eseguita la logica di convalida, anche se il controllo in questione non ha niente a che fare con il sistema di convalida. Per risolvere il problema, in ASP.NET 2.0 sono stati introdotti i gruppi di convalida, che consentono di selezionare la correlazione tra un controllo e gli elementi di convalida desiderati. Questa funzionalità è esposta mediante la proprietà ValidationGroup del controllo BaseValidator oltre che sui controlli che possono determinare l'invio di un modulo, ad esempio Button. Se eseguito in ASP.NET 2.0, HipValidator acquisisce istantaneamente questa funzionalità, poiché deriva da BaseValidator.

AudioHipChallenge

AudioHipChallenge utilizza il modulo di gestione di sintesi vocale (TTS) distribuito con il sistema operativo Windows per generare un file WAV che servirà a sottoporre un rompicapo sonoro all'utente. Per superare il test, l'utente dovrà digitare la parola ascoltata. Per risolvere il quesito, un aggressore automatizzato dovrebbe poter analizzare il file WAV per identificare la parola mediante un software di riconoscimento vocale. Idealmente, il file WAV dovrebbe essere generato in modo da rendere l'operazione difficile per l'aggressore pur continuando a consentire l'accesso agli utenti non automatici.

Come per ImageHipChallenge, la classe AudioHipChallenge funziona con un IHttpHandler, in questo caso AudioHipChallengeHandler. AudioHipChallenge è utilizzato come controllo nella pagina in cui viene eseguito il rendering del quesito HTML al client, mentre AudioHipChallengeHandler genera file audio WAV in base alle informazioni della stringa di query di cui AudioHipChallenge esegue il rendering.

RenderChallenge è il metodo principale del controllo AudioHipChallenge, che acquisisce l'ID del rompicapo e ne esegue il rendering per il browser. Come abbiamo già visto, la generazione del campo nascosto in cui viene memorizzato il contenuto crittografato avviene mediante la classe base HipChallenge, per cui a RenderChallenge è affidata la sola generazione dei controlli specifici per la visualizzazione del rompicapo in questione.

protected override void RenderChallenge(Guid id, string content)
{
    // Get the url to the audio
    string url = null;
    try
    {
        // If it's a valid URL, go with it.
        new Uri(RenderUrl);
        url = RenderUrl;
    }
    catch{}
    // If a fully-qualified URL wasn't supplied, treat it as relative
    if (url == null)
    {
        string appPath = Page.Request.ApplicationPath;
        url = Page.Request.Url.GetLeftPart(UriPartial.Authority) +
            appPath + (appPath.Length > 0 ? "/" : "") + 
            RenderUrl + "?" + ID_KEY + "=" + id.ToString("N");
    }

    // Add the WMP player control to the output
    string wmpId = "wmp" + Guid.NewGuid().ToString("N");
    HtmlGenericControl player = new HtmlGenericControl("object");
    player.Attributes["ID"] = wmpId;
    player.Attributes["CLASSID"] = 
        "CLSID:6BF52A52-394A-11d3-B153-00C04F79FAA6";
    player.Attributes["height"] = "1";
    player.Attributes["width"] = "1";
    player.InnerHtml = 
        "<PARAM name=\"URL\" value=\"" + url + "\">" +
        "<PARAM name=\"autoStart\" value=\"" + _autoStart + "\">";
    Controls.Add(player);

    // Add a button to play the sound
    if (_showPlayButton)
    {
        Button playButton = new Button();
        if (!this.Width.IsEmpty) playButton.Width = this.Width;
        if (!this.Height.IsEmpty) playButton.Height = this.Height;
        playButton.Text = Text;
        playButton.EnableViewState = false;
        playButton.CausesValidation = false;
        playButton.Attributes["OnClick"] = 
            wmpId + ".controls.play(); return false;";
        Controls.Add(playButton);
    }
}

L'HTML di cui viene eseguito il rendering nel browser mediante questo metodo consiste in un tag Object che consente di creare un controllo Windows Media Player incorporato sul lato client. L'URL cui fa riferimento il lettore multimediale viene impostato in base al valore della proprietà AudioHipChallenge.RenderUrl (perché la connessione a WMP vada a buon fine deve trattarsi di un URL completo) e deve essere configurato in modo che il file WAV venga eseguito automaticamente quando la pagina viene caricata mediante la proprietà AudioHipChallenge.AutoStart. Se la proprietà AudioHipChallenge.ShowPlayButton è true, viene eseguito il rendering di un controllo Button aggiuntivo, configurato per eseguire il file WAV quando viene fatto clic sul pulsante, consentendo all'utente di riascoltare più volte il rompicapo sonoro.

Affinché il controllo incorporato del browser sia in grado di richiamare il file WAV, è necessario mappare AudioHipChallengeHandler come IHttpHandler per tutte le richieste per l'URL specificato nella proprietà RenderUrl. Come per ImageHipChallengeHandler, è possibile configurare in questo senso il file Web.config per la soluzione ASP.NET.

<httpHandlers>
    <add verb="*" path="AudioHipChallenge.aspx"
        type="Msdn.Web.UI.WebControls.AudioHipChallengeHandler, Hip"/>
</httpHandlers>

L'implementazione di ProcessRequest di AudioHipChallengeHandler è molto semplice. L'ID del rompicapo viene recuperato dalla stringa di query e viene utilizzato per recuperare dalla cache il testo del rompicapo. Se il testo è disponibile, vale a dire se l'ID è valido e il contenuto associato non è scaduto, viene creato un file temporaneo in cui memorizzare il WAV. I dati WAV vengono creati dalla parola del rompicapo decrittografata e il file temporaneo viene trasmesso al client e successivamente eliminato, per non lasciarlo inutilmente nel file system.

public void ProcessRequest(HttpContext context)
{
    // Get the challenge text
    string text = HipChallenge.GetChallengeText(new Guid(
        context.Request.QueryString[AudioHipChallenge.ID_KEY]));

    if (text != null)
    {
        // Get a path for the temporary audio file.
        FileInfo tempAudio = new FileInfo(Path.GetTempPath() + "/" +
            "aud" + Guid.NewGuid().ToString("N") + ".wav");
        try
        {
            // Speak the data to the file
            SpeakToFile(text, tempAudio);

            // Send the audio to the client
            HttpResponse resp = context.Response;
            resp.ContentType = "audio/wav";
            resp.WriteFile(tempAudio.FullName, true);
        }
        finally
        {
            // Delete the temporary audio file
            tempAudio.Delete();
        }
    }
}

Esaminando AudioHipChallengeHandler.SpeakToFile si nota immediatamente che il codice necessario per l'esecuzione di un'attività obiettivamente complicata è minimo. Fortunatamente non ho dovuto scrivere da me un modulo di gestione TTS e ho potuto utilizzare le librerie vocali fornite da Microsoft, già disponibili nel mio sistema. Queste librerie sono esposte a livello di programmazione come un insieme di componenti COM, che sono facilmente accessibili da un client .NET grazie alle meraviglie dell'interoperabilità COM.

Figura 7. Importazione della libreria di oggetti vocali Microsoft

Utilizzando il file tlbimp.exe contenuto nell'SDK di .NET Framework o l'opzione "Aggiungi riferimento..." di Visual Studio .NET, è possibile importare la libreria di oggetti vocali Microsoft (sapi.dll) nel progetto (per impostazione predefinita, il wrapper verrà denominato Interop.SpeechLib.dll), come mostrato nella figura 7.

SpVoice è la classe base necessaria per TTS. Per prima cosa recupero un elenco delle voci installate nel mio sistema, utilizzando SpVoice.GetVoices. Questo metodo restituisce un insieme ISpeechObjectTokens da cui scelgo in modo casuale una voce da memorizzare nella proprietà Voice dell'oggetto SpVoice. Quindi un SpAudioFormat da utilizzare con SpVoice viene creato e configurato per utilizzare il compressore audio mono GSM610 a 11kHz. I file WAV generati mediante questo compressore hanno dimensioni discretamente piccole e presentano il vantaggio collaterale, per i nostri scopi, di distorcere la voce generata. Infine, viene generato un SpFileStream per un file basato su disco e il metodo SpVoice.Speak viene utilizzato per eseguire la sintesi vocale del testo specificato e scriverla nel file.

private void SpeakToFile(string text, FileInfo audioPath)
{
    SpFileStream spFileStream = new SpFileStream();
    try
    {
        // Create the speech engine and set it to a random voice
        SpVoice speech = new SpVoice();
        ISpeechObjectTokens voices = 
            speech.GetVoices(string.Empty, string.Empty);
        speech.Voice = voices.Item(NextRandom(voices.Count));

        // Set the format type to be heavily compressed.
        SpAudioFormatClass format = new SpAudioFormatClass();
        format.Type = SpeechAudioFormatType.SAFTGSM610_11kHzMono;
        spFileStream.Format = format;

        // Open the output stream and speak to it
        spFileStream.Open(audioPath.FullName,
            SpeechStreamFileMode.SSFMCreateForWrite, false);
        speech.AudioOutputStream = spFileStream;
        speech.Rate = -5; // Ranges from -10 to 10
        speech.Speak(text, SpeechVoiceSpeakFlags.SVSFlagsAsync);
        speech.WaitUntilDone(System.Threading.Timeout.Infinite);
    }
    finally
    {
        // Close the output file
        spFileStream.Close();
    }
}

AudioHipChallenge fornisce anche la proprietà SpellWords, che può essere utilizzata per forzare il controllo a generare un file audio in cui la parola viene compitata piuttosto che pronunciata. Questo è possibile grazie all'override del metodo HipChallenge.ChooseWord che seleziona la parola successiva.

protected override string ChooseWord()
{
    // Get a word
    string word = base.ChooseWord();

    // If the user has opted to have words spelled, generate
    // a string that contains the spelling and return that instead.
    if (_spellWords) 
    {
        char [] letters = word.ToCharArray();
        StringBuilder sb = new StringBuilder(letters.Length*3);
        foreach(char letter in letters)
        {
            int pos = (int)(Char.ToLower(letter) - 'a');
            if (pos >= 0 && pos < 26)
            {
                sb.Append(_spelledLetters[pos]);
                sb.Append("; ");
            }
        }
        return sb.ToString();
    }
    // Otherwise, just return the word
    else return word;
}

Il metodo di cui si esegue l'override esegue una chiamata del metodo base per ottenere la prossima parola. Se la proprietà SpellWords è stata impostata su true, la stringa della parola viene suddivisa nei caratteri che la compongono e viene generata una nuova stringa con tutte le lettere separate da punto e virgola, obbligando il modulo di gestione TTS a pronunziare le lettere una per una. Purtroppo, il modulo di gestione TTS non pronuncia i caratteri come avrei voluto, anche se la pronuncia è sensata per alcuni scenari. Per far sì che la pronuncia sia quella che intendo ottenere, ho creato una matrice di stringhe che corrispondono alla pronuncia di ciascuna lettera: "hay" per 'a', "bee" per 'b', e così via.

private static string [] _spelledLetters = 
{
    "hay", "bee", "see", "dee", "ee", "ef", "gee", 
    "haych", "eye", "jay", "kay", "el", "em", "en", 
    "oh", "pee", "queue", "are", "es", "tee", "you", "vee", 
    "double you", "ex", "why", "zee"
};

La matrice è composta da 26 elementi, uno per ogni lettera dell'alfabeto inglese, memorizzati in ordine alfabetico. Così, per recuperare la stringa corrispondente alla pronuncia di una determinata lettera, non devo far altro che sottrarre 'a' dalla versione in minuscolo di tale lettera e ottengo l'indice corretto nella matrice.

E questo è tutto per l'implementazione del controllo. Dal punto di vista di uno sviluppatore, l'utilizzo di AudioHipChallenge è semplice e diretto. Un'istanza del controllo viene aggiunta a una pagina insieme a una TextBox in cui l'utente può immettere la soluzione. Un HipValidator viene aggiunto alla pagina e la relativa proprietà ControlToValidate viene impostata sull'ID della TextBox mentre per la proprietà HipChallenge viene impostato l'ID del controllo AudioHipChallenge. Infine, nell'handler dell'evento Load della pagina si aggiungono le parole all'insieme AudioHipChallenge.Word. Semplice e facile da usare.

Quella descritta è un'implementazione semplice di un rompicapo sonoro, che lascia ampi margini per ulteriori esplorazioni, se si desidera realizzarne una più robusta. Si potrebbe, ad esempio, sovrapporre il testo parlato a una conversazione di sfondo e aggiungere un'eco, per frustrare i tentativi anche del miglior modulo di riconoscimento vocale. Un altro aspetto da valutare è se gli utenti sono in grado di comprendere le parole pronunciate. Se si opta per la lettura delle singole lettere, conviene verificare che lettere dal suono simile, ad esempio 'B' e 'P' o 'D' e 'T' non vengano confuse. Un'alternativa potrebbe essere la lettura di numeri.

Funziona?

Un'indagine su Internet ha rivelato che è in corso un'intensa attività di ricerca mirata alla violazione di questo tipo di rompicapo, con discrete percentuali di successo. Il cosiddetto rompicapo "EZ-Gimpy", come quello utilizzato da Yahoo, è stato violato dai ricercatori con una percentuale di successo di ben il 93% (http://www.cs.berkeley.edu/~mori/gimpy/gimpy.html), mentre i rompicapo TTS più semplici, come quello creato in questo articolo, sono stati risolti dai sistemi di riconoscimento automatico quasi altrettanto facilmente che dagli esseri umani (http://csdl.computer.org/comp/proceedings/ictai/2003/2038/00/20380226abs.htm). Dovremmo quindi arrenderci e cestinare tutto? Probabilmente no. Anche se un computer è in grado di risolverli, i rompicapo rappresentano comunque un ostacolo in più per i pirati informatici. Gli sviluppatori di programmi automatici studiati per attaccare un sito protetto da CAPTCHA dovranno incorporare moduli di gestione del riconoscimento vocale in applicazioni e script, cosa che richiederebbe un significativo investimento di risorse. Inoltre, i moduli di riconoscimento basati su computer richiedono l'utilizzo esteso di calcoli, un altro aspetto che fa aumentare il costo di un attacco. Alla fine queste soluzioni automatiche potrebbero essere distribuite a "script kiddies" su Internet: a questo punto il rompicapo violato potrebbe essere modificato e reso più difficile da risolvere, e il ciclo ricomincerebbe. Come molti altri problemi correlati, si tratta di una specie di corsa agli armamenti tra aggressori e difensori.

Sono stati ipotizzati altri attacchi in cui, invece di tentare di violare un determinato rompicapo, si sfrutta manodopera a basso costo. Dopo tutto, se un problema è difficile da risolvere per un computer, perché non pagare delle persone per farlo? Si potrebbe immaginare una sala piena di impiegati al minimo salariale che passano tutto il giorno a risolvere questi rompicapo, ma non sembra una soluzione particolarmente conveniente per un pirata informatico. Non è detto però che un pagamento debba necessariamente essere in denaro e si potrebbe pensare a un sistema di baratto. Un classico esempio è il seguente: il pirata informatico allestisce un sito Web per soli adulti e offre la possibilità di visionarne gratuitamente il materiale a chiunque accetti di risolvere un rompicapo. Quindi, trasferisce il rompicapo dal sito oggetto del suo attacco all'utente del sito per adulti, con la richiesta di risolverlo, dopodiché reinvia la soluzione al sito attaccato. Anche se è certamente fattibile, questa soluzione richiede comunque un considerevole investimento di risorse e, a meno che il sito per adulti non sia molto trafficato, può essere inficiata da alcune delle misure difensive descritte in questo articolo, ad esempio la definizione di un intervallo di tempo molto breve prima della scadenza del rompicapo.

Anche gli attacchi di tipo Denial of Service (DoS) possono rappresentare un problema quando si tratta di pagine che richiedono l'utilizzo di risorse consistenti sul server. La logica di generazione sia dell'immagine che dell'audio presentata in questo articolo richiede una notevole quantità di cicli della CPU e pertanto, come per ogni altra pagina Web che implichi un utilizzo intenso di risorse, un pirata informatico potrebbe provare a sferrare un attacco di tipo DoS, inondando di richieste gli endpoint ImageHipChallengeHandler e AudioHipChallengeHandler. Fortunatamente esistono diverse soluzioni plausibili per impedire attacchi di questo tipo. Una potrebbe essere memorizzare nella cache i rompicapo per un certo periodo di tempo, tale che le richieste relative a un rompicapo, dato lo stesso testo, produrrebbero la stessa immagine. Il timeout della cache dovrebbe essere sufficientemente piccolo da evitare che altri pirati informatici possano trarne vantaggio. Un'altra soluzione cui a volte si ricorre per limitare l'impatto degli attacchi DoS sul server consiste nell'aggiungere un ritardo casuale ma relativamente significativo alla risposta della pagina Web eseguendo un Thread.Sleep prima di qualsiasi elaborazione. Per un utente normale, un secondo di attesa in più per ricevere il risultato della richiesta non ha alcuna importanza, ma un pirata informatico che tenti di bloccare la CPU al 100% avrà serie difficoltà a farlo.

Come si è già rilevato in precedenza, i rompicapo HIP sono difficili da imbroccare e prima o poi verranno violati. Di conseguenza, l'implementazione richiede un lavorio continuo perché sia i rompicapo che gli attacchi diventano sempre più sofisticati: ragione in più per realizzare un framework HIP in cui sia possibile inserire nuovi rompicapo all'occorrenza e senza troppa fatica. Ciò significa che le soluzioni HIP devono essere sorvegliate con attenzione e monitorate attivamente per rilevare eventuali attacchi andati a buon fine e problemi di utilizzabilità. Tutto concorre a consigliare la centralizzazione di una soluzione valida in un servizio ospitato, di cui il sito potrà quindi usufruire. Ma fino a quando un servizio del genere non sarà disponibile, affidabile, collaudato alla prova del tempo e conveniente dal punto di vista economico, gli approcci descritti in questo articolo potranno aiutarvi a realizzare una soluzione HIP e a utilizzarla a regime.

Non va dimenticato che se è vero che gli attacchi contro i sistemi di questo tipo esistono (come per quasi tutti i sistemi), nelle esperienze di distribuzione reali i CAPTCHA si sono dimostrati molto efficaci per siti di ogni dimensione, fungendo da deterrente per tutti i pirati informatici salvo i più determinati.

Naturalmente, gli argomenti a sfavore di questi sistemi non riguardano solo le loro vulnerabilità. Alcune persone trovano i rompicapo semplicemente fastidiosi e tendono a non utilizzare i siti che li propongono. Prima di procedere all'implementazione di un sistema HIP sul vostro sito, potreste trovare interessante la lettura di alcuni documenti che illustrano i progressi più attuali in questo campo. Nell'articolo di Pinkas e Sander citato all'inizio di questo articolo sono esposte diverse idee valide per rendere più piacevole l'esperienza degli utenti, soprattutto nei casi in cui i CAPTCHA vengono utilizzati nelle procedure di accesso.

Nessun articolo sui CAPTCHA sarebbe completo senza accennare alle limitazioni che le soluzioni di questo tipo impongono dal punto di vista dell'accessibilità. Attualmente, la generazione delle soluzioni ai rompicapo CAPTCHA è affidata ai sensi dell'essere umano. I rompicapo visivi richiedono una vista buona, quelli sonori un buon udito e ovviamente gli uni o gli altri, o entrambi i tipi, creerebbero difficoltà a un numero non trascurabile di persone. Per un'analisi più approfondita consultare il documento Inaccessibility of Visually-Oriented Anti-Robot Tests, del W3C, disponibile in linea all'indirizzo http://www.w3.org/TR/turingtest/). Alcuni rompicapo visivi possono creare problemi anche alle persone vedenti; uno basato, ad esempio, sulla capacità dell'utente di distinguere tonalità di verde e di rosso creerebbe difficoltà a circa il 10% della popolazione maschile, affetta da daltonismo. Consentire la presentazione di diversi tipi di rompicapo, dando all'utente la possibilità di scegliere in base alle proprie capacità, è importante per evitare di precludere l'accesso al sito a una parte degli utenti. Oggi la maggior parte degli utenti del Web è in grado di utilizzare in modo soddisfacente almeno uno dei sensi richiesti e ha quindi la capacità di risolvere un rompicapo sonoro o uno visivo, ma non sarà sempre così, perché le caratteristiche di accessibilità dei computer e del Web migliorano costantemente. Per il momento, tutti i siti che scelgono di utilizzare CAPTCHA dovrebbero consentire agli utenti di scegliere il tipo di rompicapo da risolvere. È possibile che in futuro vengano inventati nuovi tipi di rompicapo che non richiedano l'uso della vista né dell'udito, magari basati sull'olfatto, sul gusto e perfino sulla logica invece che sulla presentazione del rompicapo. Certamente, se Turing aveva ragione, i computer raggiungeranno infine uno stato di sofisticazione tale da non consentire più a un essere umano di distinguerli da un suo simile. A quel punto tutti questi sistemi diverranno obsoleti. A meno che i computer non divengano più intelligenti degli esseri umani, perché a quel punto forse un computer potrà ancora distinguere un essere umano da un altro computer, anche se una persona non sarà in grado di farlo. C'è da riflettere.

Pubblicazioni correlate


Stephen Toub è direttore tecnico di MSDN Magazine, su cui tiene anche la rubrica .NET Matters.

 

© 2004 Microsoft Corporation. Tutti i diritti riservati. Note legali.