Czasami w widoku wywołujemy funkcję JavaScript z parametrami, które są przekazane za pomocą ViewModel z kontrolera. Załóżmy, że nasza metoda w kontrolerze wygląda następująco:
public ActionResult Index() { return View(new FooModel("Hello World!")); }
ViewModel z kolei zawiera jedną właściwość:
public class FooModel { public string Text { get; set; } public FooModel(string text) { Text = text; } }
Następnie zdefiniujmy jakąkolwiek funkcję JS przyjmującą parametry:
<script type="text/javascript"> function doSomethingInJs(text) { $('#sampleContainer').html(text); } </script>
Błędne wywołanie funkcji w razor wygląda z kolei następująco:
<script type="text/javascript"> doSomethingInJs('@Model.Text'); </script>
Jest to nic innego, jak klasyczny przykład wstrzyknięcia złośliwego kodu. Dotyczy to wszystkich pól typu string. Nawet jeśli spodziewamy się tylko imienia czy nazwiska, nigdy nie wiadomo jakie dane zawiera view model. Zwykle dane pochodzą z bazy danych. Co jeśli w jakiś sposób, baza danych zawiera już złośliwy kod? Aplikacja powinna traktować wszystkie dane, jako potencjalne zagrożenie.
Co jeśli, następująca treść zostanie przekazana?
public ActionResult Index() { return View(new FooModel("<script>alert('Hello World')</script>")); }
Na szczęście powyższy kod jest bezpieczny, ponieważ Razor zamieni <script> na znaczniki HTML:
<script type="text/javascript"> $(function() { doSomethingInJs('<script>alert('Hello World')</script>'); }); </script>
Innymi słowy, kod zostanie wyświetlony jak zwykły tekst. Za każdym razem, jak wywołujemy @Model, zawartość jest enkodowana. Co jednak stanie się, jeśli treść zostanie przekazana jako ASCII HEX?
public ActionResult Index() { var text = "\\x3c\\x73\\x63\\x72\\x69\\x70\\x74\\x3e\\x61\\x6c\\x65\\x72\\x74\\x28\\x27\\x48\\x65\\x6c\\x6c\\x6f\\x20\\x57\\x6f\\x72\\x6c\\x64\\x27\\x29\\x3c\\x2f\\x73\\x63\\x72\\x69\\x70\\x74\\x3e"; return View(new FooModel(text)); }
Razor nie zakoduje treści ponieważ jest to zwykły tekst (brak znaczników HTML). Javascript z kolei, rozpoznaje ASCII w postaci hex (za pomocą \x). Efekt będzie taki, że kod javascript wykona się i wyświetli alert. W tej chwili, jeśli zajrzymy do źródła strony, zobaczymy:
<script type="text/javascript"> $(function() { doSomethingInJs('\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x27\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x27\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e'); }); </script>
Rozwiązaniem jest endkodowanie parametrów funkcji JavaScript za pomocą HttpUtility.JavaScriptStringEncode:
<script type="text/javascript"> $(function() { doSomethingInJs('@HttpUtility.JavaScriptStringEncode(Model.Text)'); }); </script>
Efektem będzie:
<script type="text/javascript"> $(function() { doSomethingInJs('\\x3c\\x73\\x63\\x72\\x69\\x70\\x74\\x3e\\x61\\x6c\\x65\\x72\\x74\\x28\\x27\\x48\\x65\\x6c\\x6c\\x6f\\x20\\x57\\x6f\\x72\\x6c\\x64\\x27\\x29\\x3c\\x2f\\x73\\x63\\x72\\x69\\x70\\x74\\x3e'); }); </script>
Widzimy, że każdy kod ascii, zostały poprzedzony dodatkowym znakiem ‘\’.
Generalnie połączenie jQuery + mieszanie C# z JavaScript jest nie tylko złą praktyką z punktu widzenia bezpieczeństwa, ale również nie jest to zbyt czysty kod. Nie powinno mieszać się kodu inline z szablonami razor. Te dwie warstwy powinny rozwijać się niezależnie. Da nam to nie tylko łatwiejszy w utrzymaniu kod, ale również jak widać powyżej, łatwiej będzie uniknąć luk w bezpieczeństwie.
A jaka jest alternatywa?
Skoro nie powinno się mieszać widoków razorowych z JS to jak w takim razie robić to “po bożemu”?
Artykul jak przekazac dane z controllera do js w widoku, a rozwiazanie to : nie przekazywac? o_O
Mając ostatnio dosyć podobnych tworów zacząłem wykorzystywać data-attributes w html, dzięki czemu można nie mieć na widoku js’a
Piotrek, może wrzucisz posta z przykładami tego rozwiązania? Przy okazji ciekaw też jestem jak to wpłynęło na organizację kodu js w projekcie. Ja właśnie z powodu przekazywania wartości z ViewModel/Model do js bardzo dużo kodu js musiałem trzymać w widoku a chętnie bym to rozdzielił.
Małe sprostowanie:.
1. Umieszczać kod JavaScript w osobnych plikach, a nie w widoku. Łatwiej będzie nam zapanować nad tym oraz jest to bezpieczniejsze. Dlaczego? Jeśli kod jest w pliku .js, przypadkowo nie wstrzykniemy kodu z ViewModel. W szablonie Razor zbyt łatwo można popełnić błąd.
2. Jeśli mamy grubą warstwę prezentacji JavaScript, wtedy lepiej pomyśleć nad AngularJs,Knockout js itp. Szukanie elementów za pomocą jQuery i wstrzykiwanie treści to czasami przypomina użycie instrukcji GOTO – skakanie po DOM. Po za tym jak widać, jQuery może okazać się niebezpieczny. Oprócz tego zyskamy wysoką testowalność. Jakby wszystko było inline, wtedy oczywiście jest bałagan.
3. Załóżmy, że mamy bardzo cienką warstwę JS i po prostu chcemy przekazać parametr bezpośrednio z ViewModel tak jak pokazałem to w poście. Wtedy ciała funkcji itp, tez wrzucamy do osobnego pliku. Wywołanie to krytyczny moment i tam korzystamy z kodowania, które pokazałem w powyższym wpisie. Skoro warstwa JS jest cienka, wtedy możemy na tym zapanować. W przeciwnym wypadku, lepiej odseparować razor od JS i skorzystać np. z Konckoutjs + REST.