.NET, MVC, secureURL, zabezpieczenie adresu URL przed nieautoryzowaną modyfikacją
Strona główna--artykuly--dotnet_mvc_secureurl
  

.NET, MVC 3, secureURL, zabezpieczenie adresu URL przed nieautoryzowaną modyfikacją

  1. Geneza
  2. Parametry w wywołaniu metody kontrolera w .NET MVC
  3. Rozszerzanie helpera URL
  4. Atrybut zabezpieczający metody kontrolera
  5. Wykorzystanie atrybutu do zabezpieczenia konkretnej metody kontrolera

1.Geneza

Niedawno miałem okazję napisać kilka niedużych aplikacji opartych na .NET MVC 3. Co prawda aplikacje te nie miały być wystawione do internetu, a tylko miały sobie pracować w stosunkowo bezpiecznej sieci wewnętrznej ale i tak aplikacja ta musiała zostać zabezpieczona przynajmniej przed niektórymi rodzajami ataków i możliwych nieautoryzowanych manipulacji jakich mogli dokonać zbyt uświadomieni pracownicy.

Jeśli chodzi o zapytania typu POST to w .NET MVC 3 jest fajny mechanizm sprawdzający itegralność przekazywanych w nagłówku danych. Jak większość zapewne wie mówię o dodawaniu do widoku wywołania funkcji @Html.AntiForgeryToken() (wersja rezor) która spowoduje dodanie do formularza specjalnego tokena zawierającego dane zapewniające integralność przekazywanych w formatce danych. Oczywiście aby sprawdzenie integralności (niezmienialności) przekazanych danych zadziałało trzeba przed definicją metody kontrolera dodać atrybut [ValidateAntiForgeryTokenAttribute]. Niestety nie ma (a w każdym razie nie znalazłem) analogicznego mechanizmu zabezpieczającego zapytania typu GET czyli dane przekazywane przez URL. Poniżej zamieszczam sposób jaki zastosowałem aby zapewnić że nikt nie zmieni parametrów ustawianych w URLu wywoływanym przez pasek adresowy przeglądarki.

2. Parametry w wywołaniu metody kontrolera w .NET MVC

Wiem wiem wszyscy to na pewno wiedzą ale dla zasady to napiszę. W .NET MVC skład adresu URL specyfikuje zarówno jaka metoda którego kontrolera zostanie wywołana ale również jakie parametry zostaną jawnie przekazane do tej metody, poza tym jak najbardziej można dodać dodatkowe parametry przekazywane w klasyczny sposób czyli przez querystring np. taki adres http://host/home/secure/5 oznacza że zostanie wywołana metoda secure z kontrolera home a do metody secure zostanie przekazany jeden parametr o wartości 5. Oczywiście sposób i ilosć przekazanych parametrów oraz interpretacja tego co w adresie url jest nazwą kontrolera metody czy parametrów zależy od ustalonego w pliku Global.asax.cs routingu używanego w danym projekcie ale przedstawiony domyślny schemat w postaci {controller}/{action}/{id} jest chyba najczęściej używany.

Jeśli teraz zbyt cwany uzytkownik aplikacji zacznie sobie modyfikować na pasku adresowym przeglądarki (najprostrzy sposób) wartość parametru np. z "5" na "6" a teoretycznie nie powinien mieć dostępu do danych znajdujących się pod identyfikatorem "6" to zmieniając ręcznie ten identyfikator zobaczy te dane. Oczywiście można sobie wyobrazić dobrze napisaną aplikacje która na niższym poziome sprawdzi poziom dostępu zalogowanego użytkownika i nie dopuści do wyświetlenia nieuprawnionemu użytkownikowi takich danych ale oczywiście można zapewnić to bezpieczeństwo już na wyższym poziomie czyli już przy sprawdzaniu czy adres url ustawiany przez aplikację przy automatycznej generacji linku <a> przy pomocy metody @Html.ActionLink() nie został ręcznie podmieniony przez "niegrzecznego" użytkownika.

3. Rozszerzanie helpera URL

Oczywiste jest że aby adres URL był w jakiś sposób zabezpieczony musimy do niego dodać coś w rodzaju kodu CRC (sumy kontrolnej) wyliczonego z całości adresu url, która umożliwi sprawdzenie po stronie serwerowej że adres tan nie był ręcznie modyfikowany. W tym przypadku odpowiednia metoda helpera URL o nazwie CustomActionLink() (o którą helpel URL został rozszerzony) służąca do generacji linków będzie do każdego linku dodawała w querystring parametr token zawierający właśnie ten wygenerowany skrót z całości adresu url. w tym rozwiązaniu schemat routingu ustawiłem tak że parametr token bedzie właśnie generowany w guerystring a nie w części path adresu URL.

Przy tak zmodyfikowanym adresie przybierze on np. postać http://domena.pl/Home/Secure/5?token=fs43are344fs43grr52g455dee434rrr33 gdzie wartość parametru "token" będzie właśnie tą zabezpieczającą resztę adresu sumą kontrolną.

Warto zapewnić że wartość tokana będzie wyliczana przy pomocy jakiegoś odpowiednio bezpiecznego algorytmu, warto oczywiście do tego używać jakiś standardowych bezpiecznych algorytmów funkcji skrótu. W powyższym szkoleniowym przypadku wykorzystany został algorytm MD5 jednak z uwagi na już wykrytą słabość algorytmu MD5 obecnie zaleca się aby do takich celów wykorzystywać już bardziej zaawansowane funkcje skrótu.

using System.Web.Routing;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.Web.Mvc.Html;
namespace System.Web.Mvc
{
    /// <summary>
    /// Klasa generująca wartość tokena zabezpieczającego
    /// Oczywiście w tym przypadku URL jest zabezpieczony mało już bezpiecznym algorytmem MD5, 
    /// ale to tylko przykład w produkcyjnych aplikacjach zalecam użycie czegoś bardziej wyrafinowanego
    /// albo przynajmniej dodanie do wartości "skrótowanej" jakiegoś skomplikowanego piasku "sand" (ciągu utrudniającego) 
    /// który spowoduje że algorytmy generujące odpowiednik się spocą
    /// </summary>
    public static class Hash
    {
        public static string GetMD5(string text)
        {
            UnicodeEncoding UE = new UnicodeEncoding();
            byte[] hashValue;
            byte[] message = UE.GetBytes(text);

            MD5 hashString = new MD5CryptoServiceProvider();
            string hex = "";

            hashValue = hashString.ComputeHash(message);
            foreach (byte x in hashValue)
            {
                hex += String.Format("{0:x2}", x);
            }
            return hex;
        }
    }
    public static class customURLExtensions
    {
        /// <summary>
        /// Metoda do generacji URLa z doklejonym tokenem zabezpieczającym
        /// </summary>
        /// <param name="urlHelper">Niejawny parametr określający obiekt jaki rozszerzamy (nie podajemy tego parametru)</param>
        /// <param name="actionName">Nazwa akcji czyli metody kontrolera</param>
        /// <param name="controllerName">Nazwa kontrolera</param>
        /// <param name="routeValues">Obiekt anonimowy zawierający parametry przekazywane do kontrolera np. new {id = 5,cos = jakistekst}</param>
        /// <returns></returns>
        public static string CustomAction(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues)
        {
            return urlHelper.Action(actionName, controllerName, urlHelper.getTokenRouteValues(actionName, controllerName, routeValues)/*routeVal*/);
        }
        /// <summary>
        /// Metoda generująca kolekcję zawierającą zarówno parametry przekazane jak i dodany dodatkowo parametr tokena zawierający unikalną wartość odzwierciedlającą 
        /// ścieżkę utworzoną z przekazywanego w URLu PATH razem z przekazanymi w ten sposób jako "friendly url" parametrami
        /// Zabezpieczeniu tokenem będą podlegały tylko parametry przekazane jako "friendly url" czyli tylko te które mają odzwierciedlenie w tabeli routingu 
        /// ustawianej w pliku Global.asax.cs czyli parametry przakazywane jako "querystring" nie podlegają zabezpieczeniu.
        /// Także sam parametr "token" będzie przekazany w querystringu a nie w PATH adresu URL
        /// </summary>
        /// <param name="urlHelper">Niejawny parametr określający obiekt jaki rozszerzamy (nie podajemy tego parametru)</param>
        /// <param name="actionName">Nazwa akcji czyli metody kontrolera</param>
        /// <param name="controllerName">Nazwa kontrolera</param>
        /// <param name="routeValues">Obiekt anonimowy zawierający parametry przekazywane do kontrolera np. new {id = 5,cos = jakistekst}</param>
        /// <returns>Kolekcja klasy RouteValueDictionary zawierająca oprócz przekazanych do niej parametrów również parametr tokena zabezpieczającego</returns>
        public static RouteValueDictionary getTokenRouteValues(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues)
        {
            //zamieniamy tu obiekt anonimowy zawierający parametry na obiekt klasy RouteValueDictionary można było oczywiście użyć wersji z przekazaniem od razu obiektu klasy RouteValueDictionary ale wygodniej się przekazuje parametry w obiektach anonimowych
            RouteValueDictionary routeVal = new RouteValueDictionary(routeValues);
            //Generujemy jak by wyglądał narmalny url bez tokena zabezpieczającego jest to o tyle ważne że metoda Action standardowego helpera "url" 
            //wygeneruje nam url z podzialem na parametry które mają się znaleść w PATH (te którym odpowiada routing) i te które mają być w querystring 
            //dzięki temu nie będziemy musieli sami analizować tabeli routingu aby dokonać tego podziału
            string curl = urlHelper.Action(actionName, controllerName, routeVal);
            //pobieramy sobie wartość hosta portu itd czyli to co leży po lewej stronie adresu url
            string host = urlHelper.RequestContext.HttpContext.Request.Url.GetLeftPart(UriPartial.Authority);
            //Na wszelki wypadek kodujemy url ponieważ po stronie atrybuty zabezpieczajacego url już będzie zakodowany
            curl = HttpUtility.UrlDecode(curl);
            //liczymy token zabezpieczający i jednocześnie dodajemy go do tablicy rutowania. Token tak na prawdę ponieważ nie mo odzwierciedleania w tablicy
            //rutowania to będzie dodany jako część QueryString a nie ścieżki.
            routeVal.Add("token", Hash.GetMD5(curl));
            return routeVal;
        }
    }
    public static class CustomLinkExtensions
    {
        /// <summary>
        /// Metoda generująca link z doklejonym tokenem zabezpieczającym.
        /// </summary>
        /// <param name="htmlHelper">Niejawny parametr określający obiekt jaki rozszerzamy (nie podajemy tego parametru)</param>
        /// <param name="linkText">Tekst z jakim będzie przedstawiany link</param>
        /// <param name="actionName">Nazwa akcji czyli metody kontrolera</param>
        /// <param name="controllerName">Nazwa kontrolera</param>
        /// <param name="routeValues">Obiekt anonimowy zawierający parametry przekazywane do kontrolera np. new {id = 5,cos = jakistekst}</param>
        /// <param name="htmlAttributes">Atrybuty jakie zostaną doklejone do linku np. new{ @class=nazwaklasy,title=tytul</param>
        /// <returns></returns>
        public static MvcHtmlString CustomActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes)
        {
            //zamieniamy tu obiekt anonimowy zawierający parametry atrybutów w kolekcji RouteValueDictionary można było oczywiście użyć wersji z przekazaniem od razu obiektu klasy RouteValueDictionary ale wygodniej się przekazuje parametry w obiektach anonimowych
            RouteValueDictionary htmlAttr = new RouteValueDictionary(htmlAttributes);
            //generujemy teraz standardową metodą Html helpera ActionLink link jednak już z parametrem zawierającym token. Ponieważ token nie jest uwzględniany w tabeli routingu więc token w URLu będzie dodany jako querystring a nie PATH
            return htmlHelper.ActionLink(linkText, actionName, controllerName, new UrlHelper(htmlHelper.ViewContext.RequestContext).getTokenRouteValues(actionName, controllerName, routeValues), htmlAttr);
        }
    }
}

Aby teraz linki były generowane z odpowiednim tokenem wystarczy wszystkie linki w widokach generować np. w taki sposób @Html.CustomActionLink("Secure","Secure","Home",new {id = 5},new {}) czyli zamiast metody helpera o nazwie ActionLink trzeba teraz używać metodę CustomActionLink.

4.Atrybut zabezpieczający metody kontrolera

Każdą metodę kontrolera trzeba zabezpieczyć przy pomocy atrybutu który sprawdzi czy dana metoda została wywołana bez naruszenia integralności adrsu url. Czyli w skórcie czy token jaki przekazany został w wywołaniu zapytania odpowiada tokenowi wygenerowanemu z części adresu url zabezpieczonej tokenem (zabezpieczane są tylko parametry przekazywane w sekcji "path" adresu URL a nie te w querystring). W tym wypadku jako nazwę atrybutu wybrałem nazwę ValidateURLToken. Oczywiście można wybrać sobie dowolną nazwę trzeba tylko oczywiście nazwać własny atrybut według własnych upodobań.

W .NET MVC 3 można by oczywiście zarejestrować atrybut w taki sposób aby ochronie podlegały wszystkie metody wszystkich controlerów, ale niestety takie rozwiązanie było by mało elastycznie ponieważ zwykle w aplikacjach występują metody które nie powinny być objęte ochroną i wtedy oczywiście linki prowadzące do tych metod muszą być generowane przy pomocy standardowej metody helpera url ActionLink i oczywiście nie nalezy przed tymi nie chronionymi metodami dodawać atrybutu

/// <summary>
/// Filter atrybutu odpowiedzialny za walidację tokena zabezpieczającego doklejanego do urla jako "?token=tokenvalue
/// Token jest wyliczany jako skrót ze ścieżki (friendlyurl) urla razem z parametrami
/// </summary>
using System.Web.Mvc;
using System;
using System.Linq;
public class ValidateURLToken : AuthorizeAttribute
{
    private AcceptVerbsAttribute _verbs;
    public ValidateURLToken(HttpVerbs verbs)
    {
        //zapisujemy jaka wartość parametru była przekazana razem z atrybutem
        this._verbs = new AcceptVerbsAttribute(verbs);
    }
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        //pobieramy jaki rodzaj zapytania ma obecny request np. czy GET czy POST
        string httpMethodOverride = filterContext.HttpContext.Request.GetHttpMethodOverride();
        //sprawdzamy czy obecne zapytanie jest takiego typu jaki zadeklarowaliśmy wstawiając atrybut przed metodę controllera
        if (!this._verbs.Verbs.Contains(httpMethodOverride, StringComparer.OrdinalIgnoreCase))
        {
            throw new Exception("Not Expected HTTP request");
        }
        //Pobieramy wartość tokena zabezpieczającego przekazanego w URLu
        string token = filterContext.RequestContext.HttpContext.Request.Params["token"];
        if (token == null)
            throw new Exception("No URL security token");
        if (token == string.Empty)
            throw new Exception("Empty URL security token");
        //Teraz wyliczymy jaką wartość ma token wyliczany z URLa obecnego zapytania
        string encodedUrl = filterContext.RequestContext.HttpContext.Request.Url.AbsolutePath;
        string calculatedToken = Hash.GetMD5(encodedUrl);
        //Porównujemy token przekazany w parametrze "token" (czyli tym który pilnuje URLa) z tokenem wyliczonym z URLa obecnie przekazanym z zapytaniem
        //w ten sposób odkryjemy czy ktoś ręcznie nie grzebał w zapytaniu
        if (calculatedToken != token)
            throw new Exception("Wrong URL security token");
        base.OnAuthorization(filterContext);
    }
}

5. Wykorzystanie atrybutu do zabezpieczenia konkretnej metody kontrolera

Wykorzystanie atrybutu zabezpieczającego jest bardzo proste, po prostu przed metodami kontrolera które mają być zabezpieczone (oczywiście linki do nich muszą być generowane z tokenem zabezpieczającym) należy umieścić nasz atrybut np. tak, [ValidateURLToken(HttpVerbs.Get)] Przy okazji w przypadku tego (mojego) atrybutu następuje sprawdzanie jaki rodzaj zapytania (POST czy GET) może wywołać tą metodę. Trochę się rozpędziłem pisząc ten atrybut bo równie dobrze można rodzaj zapytania sprawdzać przy pomocy dodatkowego wbudowanego w .NET MVC 3 atrybutu AcceptVerbs, jeśli komuś przeszkada taka nadmiarowość to oczywiście można sobie w prosty sposób usunąć uczulenie atrybutu na tą mozliwość, wystarczy tylko rezygnacja z przekazania do naszego atrybutu parametru typu HttpVerbs.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace UrlSecure.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            return View();
        }
        //zabezpieczenie wywołania akcji controllera przez token przekazywany w URLu
        //Token przekazany będzie musiał równać się tokenowi wyliczonemy co upewni nas że nikt nie grzebał ręcznie w URLu
        //GET: /Home/Secure/5?token=tokenvaluedynamiczniegenerowanyprzezmetodehelperaCustomActionLink
        [ValidateURLToken(HttpVerbs.Get)]
        public ActionResult Secure(int? id)
        {
            return View(id);
        }
    }
}
 

Dodam jeszcze że w przypadku jeśli veryfikacja poprawności tokena się nie powiedzie wywołany zostanie standardowy wyjątek Exception z komunikatem Wrong URL security token. Wyjątek ten można w .NET MVC przejąć w standardowy dla tego frameworka sposób (nie będę tego tu opisywał bo nie o tym jest ten artykuł) w celu wyświetlenia odpowiedniej strony błędu.

Właściwie nie mam już za bardzo o czym na ten temat pisać, resztę można doczytać z komentarzy w kodze źródłowym. Podzielę się tylko małą refleksją że mam nadzieję że w przyszłych wersjach tego ciekawego frameworka (.NET MVC) będzie wbudowany standardowy mechanizm zabezpieczający adresy URL przed nieuprawnioną modyfikacją. Mam nadzieję że nie wyjdę na głupka i się nie okaże że taki mechanizm w .NET MVC 3 jednak istnieje a tylko po prostu nie udało mi się go znaleść, ale chyba jednak z niezrozumiałych dla mnie powodów w .NET MVC 3 takiego mechanizmu nie ma, na co lekarstwem mam nadzieję że jest ten artykuł.



Dodaj komentarz:
Tak
Nie

Autor:ergtewr
Data: 2014-10-09 8:13
Treść:erhytrheyht

Autor:1222222222
Data: 2013-10-25 21:23
Treść:1111121111111122222222222

Autor:12222222
Data: 2013-10-25 21:22
Treść:1111111111111

Autor:Extern
Data:2012-12-30 21:53
Treść:Test czy działa

Copyright Extern