XSS og CSRF beskyttelse i ASP.NET MVC

Jeg sidder pt. og forsøger, at sikre et site mod XSS og CSRF og i den forbindelse er jeg pt. kommet frem til at flg. som minimum bør gøres.

Sikring mod XSS

Her skal man sikre sig at klienten ikke kan injicere skadelig indhold på sitet og dette indbærer, at man konstant (nærmest paranoidt) sørger for at encode sit output. Der er flere forskellige scenarier man skal forholde sig til og nogle af disse er

  1. data der genereres ind i HTML
  2. data der genereres ind i HTML-tag atributter
  3. data der genereres ind i JavaScript

Hver af disse skal håndteres forskelligt og flg. eksempler forsøger at give et bud på, hvordan dette kan gøres. Model indeholder i disse eksempler de tekster der skal genereres ind i siden i et objekt der hedder Translations. Dette er for at signalere, at teksterne kan være variable og derfor ikke kan garanteres værende passende indhold i den givne kontekst.

Ad 1: Dette sker som udgangspunkt, når man bruger @ til at generere output, dvs. at

<strong>@Model.Translations.ImportantText</strong>

som udgangspunkt er HtmlEncoded, fordi @ sørger for dette i MVC 3.

Ad 2: Her skal man gøre sig lidt ekstra umage, men der findes også en helper i frameworket til dette, så data kan indsættes i en atribut således

<a href="/"
    title="@Html.AttributeEncode(Model.Translations.Home)">
        @Model.Translations.Home</a>

Ad 3: Denne løsning ligner lidt atribut-encoding, men formatet er endnu længere

<script>
   var tekst = 
      "@Html.Raw(Ajax.JavaScriptStringEncode(
                    Model.Translations.AlertText))";
   alert(tekst);
</script>

Med dette på plads skulle der være taget hånd om de fleste scenarier mht. encoding. Det skal naturligvis gøres for ALT input der kommer fra en utroværdig kilde - endda også databasen, da den kan gemme på XSS, med mindre man har sørget for at lukke for alt indkommende skadelig indhold på alle kanaler (POSTS, COOKIES, HEADERS osv.).

Håndtering af CSRF

MVC 3 har som standard en atribut til controller actions, som tjekker for om en given forespørgsel indeholder et unikt token og hvis det ikke gør, så fejler forespørgslen helt.

Feltet hedder __RequestVerificationToken og ligger typisk i den formular der sendes med en POST-forespørgsel. Dette felt kan, af MVC frameworket, også nemt genereres og indsættes i formularer, hvor dette måtte være nødvendigt. Dette gøres således

@using(Html.BeginForm())
{
   @Html.AntiForgeryToken()
   <!-- Resten af din fomular her... -->
}

Således er der, i formularen og i en cookie, indsat et unikt token, som serveren kan tjekke på når formularen POSTes tilbage til serveren. Controlleren til formularen kunne så dekoreres således

[ValidateAntiForgeryToken]
public ActionResult Contact(ContactViewModel model)
{
   // do the contactstuff here...
   return View();
} 

Således skulle denne formular være sikret mod at contact-formularen kan sendes fra andre sites end dit eget. Hvis man vil krydre tingene endnu mere, er der også mulighed for at tilføje "salt" til sit token. Dette gøres således begge steder (i formularen og i ValidateAntiForgeryToken-atributten ifm. controller action).

Håndtering af CSRF i et AJAX-scenarium

Hvis man laver meget AJAX på en side, kan det være besværligt at håndtere CSRF, men laver man POST-request flere gange i løbet af sidens levetid, kan dette løses med lidt scripting og én omnipresent formular på siden, der indeholder AntiForgeryToken (som illustreret nedenfor).

@using(Html.BeginForm("", "", FormMethod.Post, 
              new { id = "frmARFTokenForm" }))
{
   @Html.AntiForgeryToken()
} 

I scriptet kan man så aflæse denne og plastre den ind i sin AJAX-POST-forespørgsel. Dette kunne se således ud (her antager jeg at jQuery er inkluderet i siden)

<script>
    $(function() {
       var tokenForm = "#frmARFTokenForm",
           tokenField = "input[name=__RequestVerificationToken]";
  
       $("#sendData").click(function() {
          var contactForm = $("#frmContact");
          var arfToken = $(tokenForm + " " + tokenField).val();

          $.ajax({
             type: "POST",
             url: "/home/contact",
             data: {
                text: $("textarea[name=comment]", contactForm).val(),
                __RequestVerificationToken: arfToken
            },
            success: function(data) {
                // update the page...
            }
         });
      });
   });
</script>

Dette betyder at det er samme token der sendes med alle formularer indtil siden opdateres i browseren, hvorefter frmARFTokenForm opdateres med et nyt token, som så benyttes af de forskellige scripts.

Der er naturligvis basis for en centralisering af denne feature (f.eks. en rutine til at hente ARFToken og en til at (ind)sætte ARFToken-feltet i en given formular), men princippet er i det mindste skitseret...

Comment