Prevenzione di attacchi di reindirizzamento aperti (C#)

di Jon Galloway

Questa esercitazione illustra come impedire attacchi di reindirizzamento aperti nelle applicazioni MVC ASP.NET. Questa esercitazione illustra le modifiche apportate in AccountController in ASP.NET MVC 3 e illustra come applicare queste modifiche nelle applicazioni ASP.NET MVC 1.0 e 2 esistenti.

Che cos'è un attacco di reindirizzamento aperto?

Qualsiasi applicazione Web che reindirizza a un URL specificato tramite la richiesta, ad esempio la querystring o i dati del modulo, può potenzialmente essere manomesso per reindirizzare gli utenti a un URL esterno e dannoso. Questa manomissione viene chiamata attacco di reindirizzamento aperto.

Ogni volta che la logica dell'applicazione reindirizza a un URL specificato, è necessario verificare che l'URL di reindirizzamento non sia stato manomesso. L'account di accesso usato nell'accountController predefinito per ASP.NET MVC 1.0 e ASP.NET MVC 2 è vulnerabile agli attacchi di reindirizzamento aperti. Fortunatamente, è facile aggiornare le applicazioni esistenti per usare le correzioni dalla ASP.NET MVC 3 Preview.

Per comprendere la vulnerabilità, si esaminerà il funzionamento del reindirizzamento dell'account di accesso in un progetto di applicazione Web MVC 2 predefinito ASP.NET MVC 2. In questa applicazione, tentando di visitare un'azione del controller con l'attributo [Autorizza] reindirizzerà utenti non autorizzati alla visualizzazione /Account/LogOn. Questo reindirizzamento a /Account/LogOn includerà un parametro querystring returnUrl in modo che l'utente possa essere restituito all'URL richiesto originariamente dopo aver eseguito l'accesso.

Nella schermata seguente è possibile notare che un tentativo di accesso alla visualizzazione /Account/ChangePassword quando non è stato eseguito l'accesso in un reindirizzamento a /Account/LogOn? ReturnUrl=%2fAccount%2fChangePassword%2f.

Screenshot che mostra la pagina Accesso applicazione M V C. La barra del titolo è evidenziata.

Figura 01: Pagina di accesso con un reindirizzamento aperto

Poiché il parametro ReturnUrl querystring non viene convalidato, un utente malintenzionato può modificarlo per inserire qualsiasi indirizzo URL nel parametro per eseguire un attacco di reindirizzamento aperto. Per dimostrare questo problema, è possibile modificare il parametro ReturnUrl in https://bing.com, quindi l'URL di accesso risultante sarà /Account/LogOn? ReturnUrl=https://www.bing.com/. Dopo aver eseguito l'accesso al sito, viene eseguito il reindirizzamento a https://bing.com. Poiché questo reindirizzamento non è convalidato, potrebbe invece puntare a un sito dannoso che tenta di ingannare l'utente.

Attacco open redirection più complesso

Gli attacchi di reindirizzamento aperti sono particolarmente pericolosi perché un utente malintenzionato sa che stiamo tentando di accedere a un sito Web specifico, che ci rende vulnerabili a un attacco di phishing. Ad esempio, un utente malintenzionato potrebbe inviare messaggi di posta elettronica dannosi agli utenti del sito Web in un tentativo di acquisizione delle password. Esaminiamo come funzionerebbe nel sito NerdDinner. Si noti che il sito NerdDinner live è stato aggiornato per proteggere dagli attacchi di reindirizzamento aperti.

Prima di tutto, un utente malintenzionato invia un collegamento alla pagina di accesso su NerdDinner che include un reindirizzamento alla pagina creata:

http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn

Si noti che l'URL restituito punta a nerddiner.com, che manca un "n" dalla cena di parola. In questo esempio si tratta di un dominio che l'utente malintenzionato controlla. Quando si accede al collegamento precedente, si accede alla pagina di accesso legittima NerdDinner.com.

Screenshot che mostra la home page del punto cena nerd. La barra del titolo è evidenziata e riempita con L U che punta a Nerd Diner dot com.

Figura 02: Pagina di accesso NerdDinner con un reindirizzamento aperto

Quando si accede correttamente, l'azione LogOn dell'account MVC ASP.NET MVC reindirizza l'URL specificato nel parametro querystring returnUrl. In questo caso, è l'URL immesso dall'utente malintenzionato, ovvero http://nerddiner.com/Account/LogOn. A meno che non sia estremamente vigile, è molto probabile che non si noterà questo, soprattutto perché l'utente malintenzionato è stato attento per assicurarsi che la pagina forgiata sia esattamente come la pagina di accesso legittima. Questa pagina di accesso include un messaggio di errore che richiede di nuovo l'accesso. Clumsy noi, dobbiamo aver tipizzato la nostra password.

Screenshot che mostra la pagina Nerd Dinner Log On in formato forrd, richiedendo all'utente di immettere di nuovo le credenziali. Nella barra del titolo viene evidenziato il forged U R L nella barra del titolo.

Figura 03: Schermata Di accesso NerdDinner forged

Quando si ritipa il nome utente e la password, la pagina di accesso forgiata salva le informazioni e ci invia nuovamente al sito di NerdDinner.com legittimo. A questo punto, il sito NerdDinner.com è già stato autenticato, in modo che la pagina di accesso forgiata possa reindirizzarsi direttamente a tale pagina. Il risultato finale è che l'utente malintenzionato ha il nome utente e la password e non è stato fornito a loro.

Esaminare il codice vulnerabile nell'azione LogOn accountController

Il codice dell'azione LogOn in un'applicazione MVC 2 ASP.NET è illustrata di seguito. Si noti che in caso di accesso riuscito, il controller restituisce un reindirizzamento al returnUrl. È possibile notare che non viene eseguita alcuna convalida rispetto al parametro returnUrl.

Elenco 1 : azione MVC 2 LogOn ASP.NET in AccountController.cs

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (!String.IsNullOrEmpty(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

A questo punto verranno esaminate le modifiche apportate all'azione ASP.NET MVC 3 LogOn. Questo codice è stato modificato per convalidare il parametro returnUrl chiamando un nuovo metodo nella classe helper System.Web.Mvc.Url denominata IsLocalUrl().

Elenco 2 : ASP.NET azione MVC 3 LogOn in AccountController.cs

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            FormsService.SignIn(model.UserName, model.RememberMe);
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", 
        "The user name or password provided is incorrect.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

Questa modifica è stata modificata per convalidare il parametro URL restituito chiamando un nuovo metodo nella classe helper System.Web.Mvc.Url, IsLocalUrl().

Protezione delle applicazioni MVC 1.0 e MVC 2 ASP.NET

È possibile sfruttare le modifiche ASP.NET MVC 3 nell'ASP.NET applicazioni MVC 1.0 e 2 esistenti aggiungendo il metodo helper IsLocalUrl() e aggiornando l'azione LogOn per convalidare il parametro returnUrl.

Il metodo UrlHelper IsLocalUrl() chiama in realtà un metodo in System.Web.WebPages, poiché questa convalida viene usata anche dalle applicazioni Pagine Web ASP.NET.

Elenco 3: metodo IsLocalUrl() dal ASP.NET MVC 3 UrlHelper class

public bool IsLocalUrl(string url) {
    return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(
        RequestContext.HttpContext.Request, url);
}

Il metodo IsUrlLocalToHost contiene la logica di convalida effettiva, come illustrato nell'elenco 4.

List 4 : metodo IsUrlLocalToHost() dalla classe System.Web.WebPages RequestExtensions

public static bool IsUrlLocalToHost(this HttpRequestBase request, string url)
{
   return !url.IsEmpty() &&
          ((url[0] == '/' && (url.Length == 1 ||
           (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
           (url.Length > 1 &&
            url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
}

Nell'applicazione ASP.NET MVC 1.0 o 2 si aggiungerà un metodo IsLocalUrl() all'accountController, ma si consiglia di aggiungerlo a una classe helper separata, se possibile. Verranno apportate due piccole modifiche alla ASP.NET versione MVC 3 di IsLocalUrl() in modo che funzioni all'interno di AccountController. In primo luogo, verrà modificato da un metodo pubblico a un metodo privato, poiché i metodi pubblici nei controller possono essere accessibili come azioni del controller. In secondo luogo, verrà modificata la chiamata che controlla l'host URL rispetto all'host dell'applicazione. Tale chiamata usa un campo RequestContext locale nella classe UrlHelper. Invece di usarlo. RequestContext.HttpContext.Request.Url.Host verrà usato. Request.Url.Host. Il codice seguente mostra il metodo IsLocalUrl() modificato da usare con una classe controller in ASP.NET applicazioni MVC 1.0 e 2.

Listato 5 - Metodo IsLocalUrl() modificato per l'uso con una classe controller MVC

private bool IsLocalUrl(string url)
{
   if (string.IsNullOrEmpty(url))
   {
      return false;
   }
   else
   {
      return ((url[0] == '/' && (url.Length == 1 ||
              (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
              (url.Length > 1 &&
               url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
   }
}

Ora che il metodo IsLocalUrl() è disponibile, è possibile chiamarlo dall'azione LogOn per convalidare il parametro returnUrl, come illustrato nel codice seguente.

Elenco 6 : metodo LogOn aggiornato che convalida il parametro returnUrl

[HttpPost] 
public ActionResult LogOn(LogOnModel model, string returnUrl) 
{ 
    if (ModelState.IsValid) 
    { 
        if (MembershipService.ValidateUser(model.UserName, model.Password)) 
        { 
            FormsService.SignIn(model.UserName, model.RememberMe); 
            if (IsLocalUrl(returnUrl)) 
            { 
                return Redirect(returnUrl); 
            } 
            else 
            { 
                return RedirectToAction("Index", "Home"); 
            } 
        } 
        else 
        { 
            ModelState.AddModelError("", 
            "The user name or password provided is incorrect."); 
        } 
    }
}

Ora è possibile testare un attacco di reindirizzamento aperto tentando di accedere usando un URL restituito esterno. Si userà /Account/LogOn? ReturnUrl=https://www.bing.com/ di nuovo.

Screenshot che mostra la pagina Accesso applicazione M V C. La barra del titolo è evidenziata e riempita con la restituzione esterna di U R L.

Figura 04: Test dell'azione LogOn aggiornata

Dopo aver eseguito l'accesso, viene eseguito il reindirizzamento all'azione Home/Index Controller anziché all'URL esterno.

Screenshot che mostra la pagina Indice applicazione M V V.

Figura 05: Attacco di reindirizzamento aperto sconfitto

Riepilogo

Gli attacchi di reindirizzamento aperti possono verificarsi quando gli URL di reindirizzamento vengono passati come parametri nell'URL per un'applicazione. Il modello ASP.NET MVC 3 include codice da proteggere da attacchi di reindirizzamento aperti. È possibile aggiungere questo codice con alcune modifiche a ASP.NET applicazioni MVC 1.0 e 2. Per proteggere da attacchi di reindirizzamento aperti durante l'accesso a ASP.NET applicazioni 1.0 e 2, aggiungere un metodo IsLocalUrl() e convalidare il parametro returnUrl nell'azione LogOn.