ASP.NET MVC: Cross-Site Request Forgery

CSRF jest dzisiaj bardzo dobrze znanym atakiem, ale niestety wciąż wiele aplikacji internetowych pozostaje niezabezpieczonych. W poście nie będę opisywał szczegółowo CSRF ponieważ w Internecie jest już od dawna mnóstwo informacji o tym. Chciałbym jednak pokazać jak dzięki ASP.NET MVC możemy w łatwy sposób uchronić się przed atakiem.

W MVC standardowy formularz tworzymy w następujący sposób:

@using (Html.BeginForm("Manage", "Account")) {

    <fieldset>
        <legend>Change Email Form</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
            </li> 
        </ol>
        <input type="submit" value="Change email" />
    </fieldset>
}

Następnie mamy w kontrolerze akcję implementującą obsługę formularza, np.:

[Authorize]
public ActionResult Manage(LocalPasswordModel model)
{
  // pernamenta zmiana stanu np. poprzez modyfikację bazy danych.
  
  return View(model);
}

Akcja oznaczona jest atrybutem Authorize, co oznacza, że tylko zalogowany użytkownik może ją wywołać. Atak CSRF polega na wykonaniu jakieś czynności w kontekście innego użytkownika. W tym przypadku mamy formularz do zmiany adresu email, co oczywiście powinno być wykonane wyłącznie w kontekście zalogowanego użytkownika.

Powyższy formularz jest podatny na CSRF ponieważ ktoś może podesłać użytkownikowi fałszywy formularz tzn.:

<form id="fm1" action="http://test.com/Account/Manage" method="post">
   <input name="Email" value="newEmail@domain.com" />
</form>

Jeśli użytkownik aktualnie zalogowany wywoła powyższy formularz, akcja zostanie wykonana w jego kontekście. Innymi słowy, email zostanie zmieniony bez jego wiedzy. Jedynym wyzwaniem to przekonanie użytkownika, aby wykonał powyższy formularz. Nie trzeba nawet go  jawnie wywoływać bo można to zrobić za pomocą JavaScript w metodzie OnLoad. Potrzebne jest jednak przekonanie użytkownika, aby otworzył daną stronę z podrobionym formularzem.

Pomimo, że nie da się przeprowadzić ataku bez “pomocy” samego użytkownika to takie zaprojektowanie systemu jest bardzo niebezpieczne. W końcu nie zawsze chodzi tu o jawne odpalenie strony, ale jak występuje luka XSS to wystarczy umieścić na stronie np. wcześniej podrobiony obrazek. Aplikacja musi być zaimplementowana w taki sposób,  że dana akcja zostanie wykonana zawsze w prawidłowym kontekście.

Standardowe rozwiązanie to wygenerowanie tokena i umieszczenie go w ukrytym polu formularza. Gdy użytkownik wysyła taki formularz, wtedy porównuje się token z ukrytego pola z wcześniej zapisanym tokenem (wzorcem) w sesji. Jeśli tokeny są takie same, wtedy wiemy,  że formularz to dokładnie ten sam, który sami wygenerowaliśmy. Nikt nie przeprowadzi ataku CSRF ponieważ tokenu nie da się odgadnąć. Jeśli ktoś podrobi formularz, to w akcji wykryjemy to ponieważ token nie będzie się zgadzał ze wzorcem przechowanym w sesji.

Ataki CSRF są tak pospolite, że framework ASP.NET MVC dostarcza gotowe rozwiązanie do użycia. W celu wygenerowania tokena w polu ukrytym wystarczy:

@using (Html.BeginForm("Manage", "Account")) {
     @Html.AntiForgeryToken()
    <fieldset>
        <legend>Change Email Form</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
            </li> 
        </ol>
        <input type="submit" value="Change email" />
    </fieldset>
}

AntiForgeryToken wygeneruje wspomniane ukryte pole  oraz zapisze token w ciasteczku:

...

<input type="hidden"
    name="__RequestVerificationToken">
    value="WYGENEROWANY_TOKEN"/>
    
... 
    

Następnie w danej akcji musimy umieścić atrybut ValidateAntiForgeryToken:

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public ActionResult Manage(LocalPasswordModel model)
{
 //...
}

Atrybut sprawdzi czy token występuje zarówno w ciasteczku jak i polu ukrytym. Następnie porówna czy są one dokładnie takie same. Jeśli któryś z nich nie istnieje lub nie jest taki sam to oznacza, że ktoś podrobił formularz.

Ktoś może zadać pytanie, dlaczego przechowujemy wzorzec w ciasteczku a nie sesji? Zwykle implementując samemu proste rozwiązania, wykorzystuje się sesje ponieważ jest ona bezpieczniejsza. Ciasteczko przechowywane jest po stronie klienta i wysyła  je się z każdym zapytaniem. Na szczęście token w ASP.NET MVC to nie jest prosty, losowo wygenerowany string. Framework zadba o integralność danych (podpis cyfrowy). Przechowywanie informacji w sesji zmniejszyłoby skalowalność aplikacji, a wspomniany podpis gwarantuje integralność danych. Integralność jest bardzo ważnym elementem w implementacjach gdzie stary token (wcześniej wygenerowany) nie traci ważności i wciąż może zostać wykorzystany do wysłania formularza. W taki sposób, istnieje kilka tokenów, które mogą zostać wykorzystane. Powoduje to ryzyko, że atakujący podrobi samemu tak token, że zostanie on zaliczony przez serwer jako prawidłowy. Jeśli mamy podpis cyfrowy, to atakujący nie zna oczywiście klucza prywatnego serwera web.

Jeśli chcemy wygenerować kilka tokenów w sposób całkowicie niezależny od siebie wtedy możemy użyć parametru salt tzn.:

<%= Html.AntiForgeryToken("customSalt") %>

[ValidateAntiForgeryToken(Salt="customSalt")]

Salt, tak jak to w funkcjach haszujących, daje nam możliwość wpływania na ostateczną wartość, wygenerowaną przez ASP.NET MVC.

One thought on “ASP.NET MVC: Cross-Site Request Forgery”

  1. Skoro token jest wysyłany do klienta, to czemu kod który nieświadomie wykona użytkownik nie może pobrać tej wartości używając js i wysłać jakby to był poprawny formularz?
    np.
    $(‘#__AjaxAntiForgeryForm input[name=__RequestVerificationToken]’).val().

Leave a Reply

Your email address will not be published.