Agerimi cu Asp.Net MVC Clasic, XSS și Kendo DataSourceRequest

Asemeni cangrenei numite lege și ocârmuire, în afara cărora omul modern nu mai are valoare individuală, și cu libăriile software secretul e o balanță fină între ce oferi cu titlu opțional, ce oferi cu titlu obligatoriu, ce pretinzi cu titlu opțional și, în sfârșit, ce pretinzi cu titlu obligatoriu. Ca-n banc, ai mai mult succes dacă pui și carne, nu doar căcat:

Ițic și Ștrul și-au deschis restaurante.
La Ștrul e coada zi și noapte la mititei, iar la Ițic bate vântul și, odată cu dânsul, falimentul.
– Mai Ștrul, spune-mi și mie, cum faci tu mititei, că la tine lumea da buzna și la mine nu calca nimeni.
– Pai cum să-i fac, jumătate carne, jumătate rahat.
– Ah, tu pui și carne?

Că să intru direct în… pâine, cei care au învățat inginerie software în perioada-n care încă nu devenise ceva mișto-caracașto practicat la propriu pe jenunche-n poziție de semizen, cu zâmbet hlizit până la urechi, își amintesc de vremurile mai vechi ale ASP.Net MVC, care-a-ndulcit dictatura ideologică bazată pe valori europene propovăduită de WebForms, reținând, probabil din nostalgie, câteva apucături neplăcute.

Hai că merge

Hai că merge

Una din numitele metehne este răgălia numită request validation: cum detecta că-n parametrii cererii HTTP există o fărâmă de cod HTML, jmaf, eroare, excepție, sfârșitul lumii, sabotaj al rușilor, început de anti-democrație și anti-valori-europene, adică HttpRequestValidationException. Este marginal un lucru bun și ilustrarea perfectă (una din ele), a dictonului logudurez Drumul spre Iad e pavat cu intenții bune.

Cum a ajuns la mine? Una din aplicațiile la care lucrez funcționa pe baza acestui comportament implicit (la cum înțeleg situația, oricum nu există niciun scenariu în care un utilizator să aibă vreun motiv de-a introduce conținut HTML) și sporadic mai apăreau probleme la salvarea unor elemente. Din cauza politicilor de securitate, nu am putut obține informații din jurnale decât foarte greu și foarte târziu, dar tot a fost util în confirmarea ipotezei.

Întrebându-i mă, dar ce-ați mai completat voi prin câmpurile acelea. Păi, au zis, să vezi, mai copiem text de prin e-mail-uri. Și cam ce-ar fi prin mail-urile alea? Pai asta, ailaltă și cealaltă, au răspuns. Și-unui coleg i-a venit ideea să-ntrebe: dar cumva sunt și adrese de mail în formatul Jean Valean <jean.valjean@lesmiserables.fr>? Cică da, erau, pentru că uneori mail-urile sunt primite ca forward, uneori la alt forward și copiază de-a valma o volbură de text.

Sesizați, desigur, dilema: dacă se dezactivează cu totul request validation, atunci pot primi text într-un atare format perfect valid, însă voi accepta și măselariță HTML, ceea ce nu-i de dorit. Orișicât, primul pas este totuși retezarea răgăliei, făcută așa cum se face, din web.config, nimic spectaculos (includ cod doar pentru a da bine la SEO):

<pages validateRequest="false">
...
</pages>

Problema este filtrarea conținutului astfel încât să-ndeplinească următoarele deziderate de (scuzați oximoronul) pace, prosperitate, diversitate, multiculturalitate și ecologie:

– să nu fiu nevoit să modific fiecare metodă-n parte (controller action, adică), admițând doar mici ajustări punctuale;
– să  pot prelua conținutul valid dintr-un cod HTML (adică textul propriu-zis dintre tag-uri, spre exemplu, o pretenție rezonabilă, cine știe ce altceva mai pot introduce de vreme ce-i clar că preiau conținut otova);
– să pot specifica ce tag-uri anume să fie cu totul eliminate (ex. script, style, link).

Primul punct ține de unde și cum să fie acuplat mecanismul de filtrare, ultimele două țin mai degrabă de implementarea sa propriu-zisă – cu o mică excepție, complet neinteresantă și ușor de făcut, folosind HtmlAgilityPack, motiv pentru care n-o să insist decât pe excepția menționată.

Lesne de intuit, este vorba de acceptarea e-mail-urilor formatate precum în exemplul anterior, căci și HtmlAgilityPack îl vede ca pe un tag HTML. Durerea-i că nu aș vrea să-l trateze ca atare și, urmând metoda Flondor (decât să ne băgăm în asta și să ne chinuim să ieșim, mai bine nu ne băgăm deloc), am decis că, decât să prelucrez un arbore-n care se află și nodurile false corespunzătoare adreselor de e-mail, mai bine șuntez cu totul problema și le-nlocuiesc temporar cu un marcaj special:

_tagFormEmailRegex.Replace( 
    source, 
    "[email_address]${email_addr}[/email_address]" 
);

În acest scop, am folosit (din lene, pentru consistență și pentru că oricum cine a compus-o a fost mult mai priceput ca mine) o expresie regulată derivată din cea folosită de EmailAddressAttribute, cu două amendamente:

– caută să fie-ncadrată de peștișori (adică < și  >);
– captează adresa de e-mail găsită și o asociază etichetei email_addr, pentru a putea opera înlocuirea.

După o simplă etapă de preprocesare care constă din substituirea tuturor adreselor de e-mail conform celor descrise, periez conținutul de cod HTML, apoi refac formatarea originală replantând peștișorii la locul lor:

[email_address] devine <;
[/email_address] devine >.

Pentru acuplarea mecanismului de filtrare m-am înfipt cât de devreme am putut, scriind un model binder pentru tipul System.String, adică orice valoare de tip string va fi procesată fără drept de apel. Redau mai întâi fragmentul relevant de cod, apoi o mică discuție:

public class SanitizedStringModelBinder : DefaultModelBinder
{
   public override object BindModel( 
		ControllerContext controllerContext,
		ModelBindingContext bindingContext 
	)
   {
      IUnvalidatedValueProvider asUnvalidatedProvider =
         bindingContext.ValueProvider as IUnvalidatedValueProvider;

      ValueProviderResult value = asUnvalidatedProvider != null
         ? asUnvalidatedProvider
			.GetValue( bindingContext.ModelName, true )
         : bindingContext.ValueProvider
			.GetValue( bindingContext.ModelName );

      if (value == null)
         return null;

      if (string.IsNullOrWhiteSpace( value.AttemptedValue ))
         return value.AttemptedValue;

      string originalValue = 
		value.AttemptedValue;
      string cleanValue = InputHelpers
		.StripTags( originalValue );

      return cleanValue;
   }
}

Încercara de-a accesa acel value provider ca o instanță de IUnvalidatedValueProvider provine din faptul că IValueProvider oferă doar metoda ValueProviderResult GetValue(string key). Ori, în configurația implicită a sistemului bindingContext.ValueProvider va fi o instanță-n care GetValue() este implementată astfel (ex. NameValueCollectionValueProvider, CookieValueProvider etc.):

De chemi calul, de nu-l chemi...

De chemi calul, de nu-l chemi…

Problema de căpătâi este că skipValidation este întotdeauna false, deci, indiferent dacă am dezactivat sau nu request validation, validarea va fi oricum efectuată când avem nevoie să extragem o valoare și folosim ce ni-i dat de soartă, adică de context.ValueProvider. Însă, cum obiectele standard (NameValueCollectionValueProvider, CookieValueProvider etc.) implementează și IUnvalidatedValueProvider, putem efectua scurta verificare de la care am plecat cu discuția, ocolind, deci, problema.

Dar… sfârșitul nu-i aici. În proiect se folosește librăria Kendo UI pentru ASP.NET MVC, inclusiv componente care preiau date de pe server (Tree, Grid etc.), iar metodele care le furnizează au în comun un parametru de tip DataSourceRequest cu un model binder specificat printr-un DataSourceRequestAttribute:

[DataSourceRequest] DataSourceRequest request

Buba supurândă e că model binder-ul lor accesează valorile prin bindingContext.ValueProvider, ceea ce, atunci când una din valori conține text semănând vag a HTML… ați înțeles. Soluția e de fapt una din nouă:

– sau implementăm un alt model binder pentru DataSourceRequest (lesnicioasă întreprindere, pentru că poate fi decompilat cel standard și extras codul relevant),
– sau se implementează un IValueProvider care să rezolve cu totul comportamentul deviant ce ignoră starea cerută pentru request validation.

Experimentând cu ambele, am mers până la urmă pe cea de-a doua variantă (cu toate dezavantajele specifice pe care ni le ridica) pentru că elimina și situațiile punctuale în care trebuiau modificate semnăturile metodelor. Nu o să intru-n detalii de implementare pentru că nu prezintă niciun interes.

În schimb, mai e de punctat o chichiță: chiar dacă am definit și am înregistrat un model binder pentru System.String, acesta nu era folosit pentru valorile de tip System.String[], adică pentru vectori de System.String.

Nu-mi dau seama exact dacă este gândit anume să se comporte așa, bref e că, în lipsă de alte idei, am definit și înregistrat un model binder și pentru System.String[], abordare ce-a readus lucrurile pe făgașul dorit.