CSRF czyli Cross-Site Request Forgery jest to w skrócie zmuszenie przeglądarki ofiary do wykonania nieautoryzowanej akcji (za pomocą żądania HTTP). Ofiarą jest zalogowany do serwisu użytkownik. Celem takiego ataku może być podmiana danych w formularzu lub też wykonanie innej akcji np. stworzenie konta użytkownika z uprawnieniami admina. Wypadałoby się przed tym zabezpieczyć tworząc aplikacje webową. Na szczęście mechanizm zabezpieczeń otrzymujemy w ASP.NET MVC out of the box 🙂
Pokażmy to na przykładzie. Mamy kontroler eventów, który zawiera metody Edit – Get i Post.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// GET: Events/Edit/5 [Authorize] public ActionResult Edit(string id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Event @event = _eventRepository.GetEventById(id); if (@event == null) { return HttpNotFound(); } ViewBag.LocationId = new SelectList(_locationRepository.GetLocations().OrderBy(l => l.Name), "Id", "Name"); ViewBag.TypeId = new SelectList(_eventTypeRepository.GetEventTypes().OrderBy(et => et.Name), "Id", "Name"); return View(@event); } // POST: Events/Edit/5 [HttpPost] [Authorize] public ActionResult Edit(Event @event) { if (ModelState.IsValid) { try { _eventRepository.Update(@event); _eventRepository.SaveChanges(); } catch (Exception ex) { ViewBag.Error = true; return View(@event); } } ViewBag.LocationId = new SelectList(_locationRepository.GetLocations().OrderBy(l => l.Name), "Id", "Name"); ViewBag.TypeId = new SelectList(_eventTypeRepository.GetEventTypes().OrderBy(et => et.Name), "Id", "Name"); ViewBag.Error = false; return View(@event); } |
Teraz wystarczy, że haker skłoni nas do wyświetlenia strony zawierającej formularz z auto submitem, przez niego spreparowany z wpisanymi wartościami, np. strona HTML może zawierać taki kawałek kodu:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<form id="eventForm" action="http://localhost:6875/Events/Edit/88611f5e-fb27-479d-8437-e8e1fe8dcd38" method="post"> <input id="Id" name="Id" type="hidden" value="88611f5e-fb27-479d-8437-e8e1fe8dcd38"> <div class="form-group"> <label class="control-label col-md-2" for="StartDate">Data rozpoczęcia</label> <div class="col-md-10"> <input class="form-control datepicker text-box single-line" data-val="true" id="StartDate" name="StartDate" type="date" value="2022-04-06"> <span class="field-validation-valid text-danger" data-valmsg-for="StartDate" data-valmsg-replace="true"></span> </div> </div> </form> <script type="text/javascript"> document.getElementById('eventForm').submit(); </script> |
W przykładzie tym została podmieniona data rozpoczęcia eventu. Nie jest to co prawda jakaś bardzo inwazyjna operacja, ale obrazuje działanie CSRF. W przypadku braku zabezpieczeń, data wybranego wydarzenia zostanie zaktualizowana. Z pomocą przychodzi nam atrybut [ValidateAntiForgeryToken], którym możemy udekorować naszą metodę Edit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// POST: Events/Edit/5 [HttpPost] [Authorize] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "Id,CreationDate,Name,Description,StartDate,StartTime,EndDate,EndTime,LocationId,TypeId,Objective,NumberOfAttendees,UserId")] Event @event) { if (ModelState.IsValid) { try { _eventRepository.Update(@event); _eventRepository.SaveChanges(); } catch (Exception ex) { ViewBag.Error = true; return View(@event); } } ViewBag.LocationId = new SelectList(_locationRepository.GetLocations().OrderBy(l => l.Name), "Id", "Name"); ViewBag.TypeId = new SelectList(_eventTypeRepository.GetEventTypes().OrderBy(et => et.Name), "Id", "Name"); ViewBag.Error = false; return View(@event); } |
Ponadto w widoku musi się znaleźć taki fragment kodu:
1 2 3 4 5 |
@using (Html.BeginForm()) { @Html.AntiForgeryToken() … } |
powoduje to wygenerowanie ciasteczka i klucza, który jest ukryty w formie i wygląda tak:
1 |
<input name="__RequestVerificationToken" type="hidden" value="p-mEYr9SQrDvvCGU-ness9maQwrd7iN1ei1FUd5ieKQpXjjGNQKMHyTqER9o9GDqOm4o5cqS27JRWwZBE-R2FUitBnnWI83tMTJ3lq4zrzawzDeAPFKx-3Kf3p5eI1ouRUhfsYWElXnPhGM0fRsWkw2"> |
W żądaniu POST ciasteczko jest przesyłane i za pomocą atrybutu [ValidateAntiForgeryToken] sprawdzana jest zgodność klucza. Gdy klucz nie będzie zgodny, wystąpi błąd. Dodatkowo w metodzie Edit można dodać jeszcze jedno zabezpieczenie, chodzi o binding.
1 |
public ActionResult Edit([Bind(Include = "Id,CreationDate,Name,Description,StartDate,StartTime,EndDate,EndTime,LocationId,TypeId,Objective,NumberOfAttendees,UserId")] Event @event) |
Dzięki temu określamy jakie dane mogą zostać zapisane, ponadto w żądaniu sprawdzane są typy i wartości. Poprawność danych wskaże nam właściwość ModelState.IsValid.
Podsumowując, nie należy ignorować podatności CSRF i odpowiednio się przed nią zabezpieczyć. Na szczęście ASP.NET MVC robi to za nas 🙂