Udostępnij za pośrednictwem


Zapobieganie atakom na otwarte przekierowywanie (C#)

Autor: Jon Galloway

W tym samouczku wyjaśniono, jak zapobiec atakom na otwarte przekierowanie w aplikacjach MVC ASP.NET. W tym samouczku omówiono zmiany wprowadzone w kontrolerze AccountController w ASP.NET MVC 3 i pokazano, jak można zastosować te zmiany w istniejących ASP.NET aplikacjach MVC 1.0 i 2.

Co to jest atak typu open redirection?

Każda aplikacja internetowa, która przekierowuje do adresu URL określonego za pośrednictwem żądania, takiego jak ciąg zapytania lub dane formularza, może potencjalnie zostać naruszona w celu przekierowania użytkowników do zewnętrznego, złośliwego adresu URL. Ta manipulacja jest nazywana otwartym atakiem przekierowania.

Za każdym razem, gdy logika aplikacji przekierowuje do określonego adresu URL, musisz sprawdzić, czy adres URL przekierowania nie został naruszony. Identyfikator logowania używany w domyślnym kontrolerze AccountController dla ASP.NET MVC 1.0 i ASP.NET MVC 2 jest podatny na otwarte ataki przekierowania. Na szczęście możesz łatwo zaktualizować istniejące aplikacje, aby korzystały z poprawek z ASP.NET MVC 3 (wersja zapoznawcza).

Aby zrozumieć tę lukę w zabezpieczeniach, przyjrzyjmy się, jak działa przekierowanie logowania w domyślnym projekcie ASP.NET aplikacji internetowej MVC 2. W tej aplikacji próba odwiedzenia akcji kontrolera z atrybutem [Autoryzuj] spowoduje przekierowanie nieautoryzowanych użytkowników do widoku /Account/LogOn. To przekierowanie do /Account/LogOn będzie zawierać parametr querystring returnUrl, aby użytkownik mógł zostać zwrócony do pierwotnie żądanego adresu URL po pomyślnym zalogowaniu.

Na poniższym zrzucie ekranu widać, że próba uzyskania dostępu do widoku /Account/ChangePassword po niepologowaniu powoduje przekierowanie do /Account/LogOn? ReturnUrl=%2fAccount%2fChangePassword%2f.

Zrzut ekranu przedstawiający stronę Logowanie aplikacji my M V C. Pasek tytułu został wyróżniony.

Rysunek 01. Strona logowania z otwartym przekierowaniem

Ponieważ parametr querystring ReturnUrl nie jest weryfikowany, atakujący może zmodyfikować go, aby wstrzyknąć dowolny adres URL do parametru w celu przeprowadzenia otwartego ataku przekierowania. Aby to zademonstrować, możemy zmodyfikować parametr ReturnUrl na https://bing.com, więc wynikowy adres URL logowania będzie /Account/LogOn? ReturnUrl=https://www.bing.com/. Po pomyślnym zalogowaniu się do witryny nastąpi przekierowanie do https://bing.comwitryny . Ponieważ to przekierowanie nie jest weryfikowane, może zamiast tego wskazywać złośliwą witrynę, która próbuje oszukać użytkownika.

Bardziej złożony atak typu open redirection

Otwarte ataki przekierowania są szczególnie niebezpieczne, ponieważ osoba atakująca wie, że próbujemy zalogować się do określonej witryny internetowej, co sprawia, że jesteśmy narażeni na atak wyłudzający informacje. Na przykład osoba atakująca może wysłać złośliwe wiadomości e-mail do użytkowników witryny internetowej, próbując przechwycić swoje hasła. Przyjrzyjmy się, jak to działałoby w witrynie NerdDinner. (Pamiętaj, że aktywna witryna NerdDinner została zaktualizowana w celu ochrony przed otwartymi atakami przekierowania).

Po pierwsze osoba atakująca wysyła do nas link do strony logowania w aplikacji NerdDinner, która zawiera przekierowanie do sfałszowanej strony:

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

Zwróć uwagę, że zwracany adres URL wskazuje nerddiner.com, w którym brakuje wyrazu "n". W tym przykładzie jest to domena, którą kontroluje osoba atakująca. Gdy uzyskujemy dostęp do powyższego linku, zostaniemy przekierowani do legalnej strony logowania NerdDinner.com.

Zrzut ekranu przedstawiający stronę Główną Nerd Dinner dot com. Pasek tytułu jest wyróżniony i wypełniony U R L wskazujący na Nerd Diner dot com.

Rysunek 02. Strona logowania NerdDinner z otwartym przekierowaniem

Po poprawnym zalogowaniu akcja LogOn ASP.NET MVC AccountController przekierowuje nas do adresu URL określonego w parametrze querystring returnUrl. W takim przypadku jest to adres URL wprowadzony przez osobę atakującą, czyli http://nerddiner.com/Account/LogOn. Chyba że jesteśmy bardzo czujni, jest bardzo prawdopodobne, że tego nie zauważymy, zwłaszcza dlatego, że osoba atakująca była ostrożna, aby upewnić się, że ich sfałszowana strona wygląda dokładnie tak samo jak legalna strona logowania. Ta strona logowania zawiera komunikat o błędzie z żądaniem ponownego zalogowania. Niezdarny nas, musimy błędnie wtypować nasze hasło.

Zrzut ekranu przedstawiający stronę sfałszowanego logowania do kolacji w usłudze Nerd z monitem o ponowne wprowadzenie poświadczeń przez użytkownika. Sfałszowany U R L na pasku tytułu jest wyróżniony.

Rysunek 03. Ekran logowania sfałszowany NerdDinner

Gdy ponownie wpiszemy nazwę użytkownika i hasło, sfałszowana strona logowania zapisuje informacje i wysyła nas z powrotem do legalnej witryny NerdDinner.com. W tym momencie witryna NerdDinner.com już nas uwierzytelniła, więc sfałszowana strona logowania może przekierowywać bezpośrednio do tej strony. Wynikiem końcowym jest to, że osoba atakująca ma nazwę użytkownika i hasło, i nie wiemy, że udostępniliśmy je im.

Patrząc na kod podatny na zagrożenia w akcji LogOn AccountController

Poniżej przedstawiono kod akcji LogOn w aplikacji ASP.NET MVC 2. Należy pamiętać, że po pomyślnym zalogowaniu kontroler zwraca przekierowanie do adresu returnUrl. Widać, że nie jest wykonywana żadna walidacja względem parametru returnUrl.

Lista 1 — ASP.NET akcji LogOn MVC 2 w 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);
}

Teraz przyjrzyjmy się zmianom akcji LogOn ASP.NET MVC 3. Ten kod został zmieniony w celu zweryfikowania parametru returnUrl przez wywołanie nowej metody w klasie pomocnika System.Web.Mvc.Url o nazwie IsLocalUrl().

Lista 2 — ASP.NET akcję LogOn MVC 3 w 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);
}

Ta zmiana została zmieniona w celu zweryfikowania parametru zwracanego adresu URL przez wywołanie nowej metody w klasie pomocnika System.Web.Mvc.Url. IsLocalUrl()

Ochrona aplikacji ASP.NET MVC 1.0 i MVC 2

Możemy skorzystać ze zmian ASP.NET MVC 3 w istniejących aplikacjach ASP.NET MVC 1.0 i 2, dodając metodę pomocnika IsLocalUrl() i aktualizując akcję LogOn w celu zweryfikowania parametru returnUrl.

Metoda UrlHelper IsLocalUrl() faktycznie wywołuje metodę w elemencie System.Web.WebPages, ponieważ ta walidacja jest również używana przez aplikacje ASP.NET Web Pages.

Lista 3 — metoda IsLocalUrl() z ASP.NET MVC 3 UrlHelper class

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

Metoda IsUrlLocalToHost zawiera rzeczywistą logikę weryfikacji, jak pokazano na liście 4.

Lista 4 — metoda IsUrlLocalToHost() z klasy 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"
}

W naszej aplikacji ASP.NET MVC 1.0 lub 2 dodamy metodę IsLocalUrl() do kontrolera AccountController, ale zachęcamy do dodania jej do oddzielnej klasy pomocniczej, jeśli jest to możliwe. Wprowadzimy dwie małe zmiany w ASP.NET mvC 3 w wersji IsLocalUrl(), aby działały wewnątrz kontrolera AccountController. Najpierw zmienimy ją z metody publicznej na prywatną, ponieważ metody publiczne w kontrolerach mogą być dostępne jako akcje kontrolera. Po drugie zmodyfikujemy wywołanie, które sprawdza hosta adresu URL względem hosta aplikacji. To wywołanie korzysta z lokalnego pola RequestContext w klasie UrlHelper. Zamiast tego używać. RequestContext.HttpContext.Request.Url.Host użyjemy tej funkcji. Request.Url.Host. Poniższy kod przedstawia zmodyfikowaną metodę IsLocalUrl() do użycia z klasą kontrolera w ASP.NET aplikacji MVC 1.0 i 2.

Lista 5 — metoda IsLocalUrl(), która jest modyfikowana do użycia z klasą kontrolera 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"
   }
}

Teraz, gdy metoda IsLocalUrl() jest w miejscu, możemy wywołać ją z akcji LogOn, aby zweryfikować parametr returnUrl, jak pokazano w poniższym kodzie.

Lista 6 — zaktualizowano metodę LogOn, która weryfikuje parametr 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."); 
        } 
    }
}

Teraz możemy przetestować otwarty atak przekierowania, próbując zalogować się przy użyciu zewnętrznego adresu URL powrotu. Użyjmy /Account/LogOn? ReturnUrl =https://www.bing.com/ ponownie.

Zrzut ekranu przedstawiający stronę Logowanie aplikacji my M V C. Pasek tytułu jest wyróżniony i wypełniony zewnętrznym zwrotem U R L.

Rysunek 04. Testowanie zaktualizowanej akcji LogOn

Po pomyślnym zalogowaniu nastąpi przekierowanie do akcji Home/Index Controller zamiast zewnętrznego adresu URL.

Zrzut ekranu przedstawiający stronę Indeks aplikacji My M V C.

Rysunek 05. Atak z otwartym przekierowaniem został pokonany

Podsumowanie

Otwarte ataki przekierowania mogą wystąpić, gdy adresy URL przekierowania są przekazywane jako parametry w adresie URL aplikacji. Szablon ASP.NET MVC 3 zawiera kod chroniący przed otwartymi atakami przekierowania. Ten kod można dodać z pewnymi modyfikacjami aplikacji ASP.NET MVC 1.0 i 2. Aby chronić przed otwartymi atakami przekierowania podczas logowania się do aplikacji ASP.NET 1.0 i 2, dodaj metodę IsLocalUrl() i zweryfikuj parametr returnUrl w akcji LogOn.