Hur du kan skydda dig mot Cross Site Request Forgery i ASP.NET MVC

I torsdags höll jag och Johan Lindfors ett seminarie om ASP.NET MVC-ramverket på Microsoft sommarkollo. Vi visade bland annat hur du kan skydda dig mot s.k. ‘Cross Site Request Forgery”-attacker, ofta förkortat XSRF.

XSRF innebär vanligtvis att en användare luras att klicka på en länk som går till en sida kontrollerad av hackern eller att hackern injicerar kod i en sida som användaren surfar till, t.ex. via ett formulär som har dålig input-validering. Om det finns en giltig inloggad session kan koden eller sidan sedan utföra attacker mot applikationen med användarens fulla behörighet.

I vårt exempel hade jag en väldigt enkel Action-metod som använde MVC-ramverkets inbyggda ModelBinding-funktionalitet för att mappa formulärdata mot ett domänobjekts egenskaper. I vårt fall var det ett ‘Track’-objekt:

image

När ett objekt anges som in-parameter till en Action-metod i ASP.NET MVC så kommer ramverket att försöka koppla ihop det formulärdata som postas in med rätt egenskaper på objektet genom namnen på formulärfälten.

I det här fallet så sparades sedan det inskickade objektet till databasen via ett repository:

 // ... detta är i en TrackController-klass
  
 [Authorize(Users="rfolkes")]
 [AcceptVerbs(HttpVerbs.Post)]
 public ActionResult Edit(Track track)
 {
     try
     {
         _repository.Update(track);
         return RedirectToAction("Index");
     }
     catch
     {
         return View(track);
     }
 }
  
 //...

Åtkomsten till vår Action-metod är skyddad genom att vi lagt på attributet [Authorize] och angett att endast den inloggade användaren ‘rfolkes’ ska kunna posta in data till metoden.

I vårt scenario var det två egenskaper som vi INTE ville skulle vara möjliga att editera via webben, egenskaperna IsApproved och Votes. För att lösa det så skrev vi helt enkelt inte ut dessa i den editerings-vy som visade upp formuläret:

    <% using (Html.BeginForm()) {%>
        <fieldset>
            <legend>Fields</legend>

                <%= Html.Hidden("Track.ID", Model.ID) %>
            <p>
                <label for="Track.Name">Name:</label>
                <%= Html.TextBox("Track.Name", Model.Name)%>
                <%= Html.ValidationMessage("Name", "*") %>

            </p>

            <p>

                <label for="Track.Description">Description:</label>

                <%= Html.TextBox("Track.Description", Model.Description)%>

                <%= Html.ValidationMessage("Description", "*") %>

            </p>

           

            <!-- Vi struntar i att skriva ut Track.IsApproved

                 och Track.Votes för användaren ska inte kunna

            ange dessa -->

            <p>

                <input type="submit" value="Save" />

            </p>

        </fieldset>

 

    <% } %>

Så – är detta en tillräckligt säker lösning?

Låt oss börja med det faktum att vi har åtkomstskyddat vår Action-metod. I vår demo så visade vi hur en inloggad användare kan luras att klicka på en länk efter att ha navigerat ifrån applikationen, eller efter att ha öppnat en ny tabb i webbläsaren – t.ex. via ett mail som ser ut att komma från någon betrodd person:

 haxx0rmail

På adressen https://91.189.45.77/haxx0r.htm finns en HTML-sida som innehåller följande:

 <html>
  
 <head>
  
 <script language="Javascript" type="text/javascript">                  function send3vilHaxx0rUpdate()        {              document.getElementById("form1").submit();                        }     </script>
  
 <title>hoppas du inte hinner se denna...</title>
  
 </head>
  
 <body onLoad="send3vilHaxx0rUpdate();">
  
 <form id="form1" method="post" action="https://localhost:6799/Track/Edit/1">
  
 <input id="Track_ID" name="Track.ID" type="hidden" value="1" />
  
 <input id="Track_Name" name="Track.Name" type="hidden" value="XNA Game Studio" />
  
 <input id="Track_Description" name="Track.Description" type="hidden" value="Johan Lindfors håller seminarie om XNA Game Studio" />
                 
 <input id="Track_IsApproved" name="Track.IsApproved" type="hidden" value="True" />
  
 <input id="Track_Votes" name="Track.Votes" type="hidden" value="10000" />
  
 </form>
  
 </body>
  
 </html>

Den här sidan kommer alltså att posta in värden till min Edit-Action som – om detta sker i en redan inloggad session – kommer att spara dessa värden i databasen. Eftersom även Track.IsApproved och Track.Votes finns med i det här formuläret kommer även dessa värden att uppdateras i databasen.

Så hur skyddar jag mig mot den här typen av attacker?

Till att börja med så kan du säkerställa att formulärpostningar till dina Action-metoder bara kommer från vyer i din applikation genom att använda en AntiForgeryToken:

    <% using (Html.BeginForm()) {%>

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

        <fieldset>
            <legend>Fields</legend>

                <%= Html.Hidden("Track.ID", Model.ID) %>
            <p>
                <label for="Track.Name">Name:</label>
                <%= Html.TextBox("Track.Name", Model.Name)%>
                <%= Html.ValidationMessage("Name", "*") %>

            </p>

Hjälpklassen Html:s AntiforgeryToken-metod kommer, med hjälp av saltet ‘sommarkollo09’, generera en krypterad token som skrivs ut i ett gömt fält när sidan genereras. Denna token kan sedan kontrolleras i din Action-metod för att säkerställa att formulärdata inte kan postas någon annanstans ifrån:

 // ... detta är i en TrackController-klass
  
 [ValidateAntiForgeryToken(Salt="sommarkollo09")]
 [Authorize(Users="rfolkes")]
 [AcceptVerbs(HttpVerbs.Post)]
 public ActionResult Edit(Track track)
 {
     try
     {
         _repository.Update(track);
         return RedirectToAction("Index");
     }
     catch
     {
         return View(track);
     }
 }
  
 //...

Men detta räcker inte för att skydda mot möjligheten för inloggade användare att skicka in fler fält än vad som är tillåtet. Den svaga länken ligger nu i att vi utnyttjar ASP.NET MVC-ramverkets inbyggda standard-ModelBinder. Patrik Corneliusson har skrivit en utförlig bloggpost om denna typ av problematik här.

För att skydda sig mot detta finns det några olika sätt att gå tillväga. Vi skulle kunna registrera en egendefinierad ModelBinder istället för den inbyggda och vi skulle kunna använda en ViewModel som endast innehåller de tillåtna fälten. I vårt fall använder vi oss av den inbyggda funktionen för att filtrera vad som ska bindas genom attributet BindingAttribute:

 // ... detta är i en TrackController-klass
  
 [ValidateAntiForgeryToken(Salt="sommarkollo09")]
 [Authorize(Users="rfolkes")]
 [AcceptVerbs(HttpVerbs.Post)]
 public ActionResult Edit(
     [Bind(Include="ID,Name,Description")]Track track)
 {
     try
     {
         _repository.Update(track);
         return RedirectToAction("Index");
     }
     catch
     {
         return View(track);
     }
 }
  
 //...

Observera att jag explicit angivit egenskaperna “ID,Name,Description” – d.v.s. skapat en s.k. ‘White list’, istället för att använda ‘Exclude’ och angivit vilka som inte ska få skickas in. Risken med att försöka skapa en lista över vilka som inte får skickas in (en s.k. ’Black list’) är att du glömmer något fält, eller att egenskaper tillkommer i modellen men glöms bort läggas till i den exkluderande listan.