Tpl.js vs JSlint
czwartek, 14 stycznia 2010
Na początek muszę zaznaczyć że jestem zwolennikiem weryfikacji HTML, CSS i JavaScript. Ten blog jest jedynym odstępstwem od tej zasady. Po części ze względu na jego eksperymentalny charakter, po części ponieważ jego tworzenie jest dla mnie zaworem bezpieczeństwa, ale głównie dla tego że silnik Bloggera, o który jest oparty, nie jest w stanie wygenerować poprawnych stron.
Główny powód używania walidatorów jest banalnie prosty: To co Firefox i Chrome uznają za poprawny kod, przez Operę i IE może zostać odrzucone z błędami składniowymi.
JSLint
Ostatnio pisałem o systemie szablonów stron na użytek JavaScript ( Szablony). Zamieszczony przykład nie przechodzi walidacji, za sprawą dwóch konstrukcji: eval i with. Jeśli pierwsze z nich Douglas Crockford uważa za Zło Wcielone, to drugie traktuje jak portal do piekielnych wymiarów.
Późniejsze wersje Tpl.js przekształciłem w prawdziwy kompilator, co pozwoliło wykorzystać możliwości współczesnych silników z kompilacją JIT. Powstała wersja, która przechodzi przez JSLint bez najmniejszych ostrzeżeń. Wydawało by się że narzędzie osiągnęło stan doskonały…
Czarna Magia
/*jslint white:false, evil:true (…)
Gdy niedawno przeglądałem kod, rzuciła mi się w oczy powyższa dyrektywa. Momentalnie przypomniał mi się cytat z niedawno oglądanego filmu…
— Co się tu dzieje, czy to jakaś…
— Magia. Najczarniejsza magia.
To ten fragment pomiędzy 0:20s a 0:42s
Formatowanie tekstu
Pierwsza dyrektywa white:false, wyłącza sprawdzanie formatowania. Wprowadzając ten test Crockford stara się chyba upodobnić JavaScript do języka Python. Tyle że stosowanie tak sztywnych reguł nie zawsze poprawia czytelność kodu. Oto przykład:
if (/^\{else\}$/.test(key)) return "}else{"; if (/^\{\/if\}$/.test(key)) return "}"; if (/^\{\/for\}$/.test(key)) return "})";
Do tych trzech linijek kodu zostanie wygenerowanych aż 9 ostrzeżeń (!) Poprawienie ich jest banalnie proste… ale efekt będzie odwrotny od zamierzonego – ucierpi na tym czytelność.
Funkcja, której są fragmentem, przestanie mieścić się na ekranie, zmuszając do ciągłego przewijania tekstu.
Dodatkowe klamry obniżą czytelność, przyczyniając się do powstania potworków w stylu { return "}else{"; }
. Co gorsze JSLint nie posiada żadnej opcji pozwalającej na wyłączenie tego testu.
Poprawnie
sformatowany kod, utraci swoje główne przesłanie, jakim jest uwidocznienie dużej powtarzalności instrukcji.
Podążając ścieżkami wytyczonymi przez JSLint, druga wersja Tpl.js roztyła się do 240 linii i utraciła urok swojego pierwowzoru. Gdy po pewnej przerwie, chciałem rozbudować narzędzie, wybrałem za podstawę starszą wersję, przepadło sprawdzanie składni i sensowne komunikaty o błędach, ale przyszłość stoi otworem.
Porównanie z pierwotną wersją mówi samo za siebie: tpl.js (diff).
Radykalne stosowanie reguł formatowania kodu rodem z C, gdzie występują tylko globalne definicje funkcji, kończy się utratą czytelności.
Zło…
Funkcja eval i pokrewne – tu całkowicie zgadzam się autorem JSLint. Kiedyś natknąłem się na samouczek dla początkujących, gdzie została przedstawiona jako funkcja do konwersji typów, pozwalająca na przekształcenie tekstu na wartość numeryczną… Jeśli kiedyś będzie to czytał ktoś początkujący: Nigdy tak nie rób! Chyba że pewnego dnia chcesz dowiedzieć się jaką wartość ma document.cookie
.
Pomimo krytyk, eval, a szczególnie new Function() są bardzo przydatne. Dają dostęp do kompilatora JavaScript, podobnie jak właściwość node.innerHTML daje dostęp do parsera HTML. Tpl.js jest kompilatorem szablonów, więc użycie tej konstrukcji jest jak najbardziej uzasadnione.
Co ciekawe, JSLint piętnuje użycie eval, podczas gdy odwołania do innerHTML przechodzą bez żadnych ostrzeżeń, a przecież to wywołanie odpowiada za podatność na ataki XSS wielu stron. Nie trzeba przecież być specjalnym geniuszem by spróbować czegoś podobnego:
node.innerHTML = "<script>alert('HAHA!')</script>"
lub nieco bardziej subtelnie:
node.innerHTML = "<img src='javascript:alert(document.cookie)'>"
DooM
Pozostało mi jeszcze omówienie instrukcji with, która rzeczywiście jest niczym otwarcie bram piekielnych. Problem w tym że gdy napiszemy a
, to tak na prawdę nie mamy pewności czy chodzi o naszą zmienną, czy też właściwość obiektu będącego argumentem dla with.
Tutaj jednak sytuacja wygląda nieco inaczej, ponieważ mamy do czynienia z szablonami, których działanie nie powinno mieć żadnych efektów ubocznych:
- W szablonach nie ma miejsca dla przypisań.
- Wszystkie używane nazwy powinny odnosić się do właściwości wskazanego obiektu, do zmiennych globalnych nie powinniśmy się odwoływać.
Trzymając się tych dwóch prostych zasad, możesz być w miarę bezpieczny i nie obawiać się chodzących trupów, oraz krwawych bestii. Jedyny problem sprowadza się do tego że liczący kilkadziesiąt linii kod nie wymusza tych zasad.
Na zakończenie
Ostatnie skojarzenie…
To co napisałem, jedynie uzasadnia wybór rozwiązań, nie daje jednak odpowiedzi na pytanie, które skłoniło mnie do napisania tego tekstu. Czy programy, które piszę są czarną magią? Widząc miny laików, wiem że wielu z nich jest o tym przekonanych – ale takie jest powszechne postrzeganie współczesnej technologii.
Mogę jedynie napisać że jest to całkowicie bezpieczne, pod warunkiem że będzie używane zgodnie z instrukcją obsługi. Oczywiście żadnych gwarancji, używasz tego na własną odpowiedzialność.
Odnośniki
- JSLint Walidator JavaScript (strona opisująca działanie).
- Tpl.js. Kompilator szablonów dla JavaScript (64 linie kodu).
Etykiety: B, javascript, json, programowanie, R, rozważania, szablony
Red 20:51
Komentarze
Jednak… Mimo że przeglądarka nie wykona skryptu, to taki węzeł zostanie wstawiony do dokumentu. Poczucie bezpieczeństwa może być złudne, zależnie od tego co będzie się działo dalej z tak spreparowanym dokumentem.
To nie wszystko. Gdy celem jest IE atakujący ma ułatwione zadanie, wystarczy by dodał atrybut defer="true".
Właśnie tego rodzaju haki sprawiają że ta przeglądarka jest tak chętnie wykorzystywana przy wszelkiego rodzaju atakach.
W świetle ostatniego ataku na Google, to co kiedyś pisałem o tej przeglądarce (Duchy Internetu) nabiera coraz bardziej ponurego wydźwięku.
Z tym zastrzeżeniem, że tak jak to napisałeś spowoduje błąd bo głupi JavaScript "pomyśli", że </script> kończy skrypt ;-) node.innerHTML"<script>alert('HAHA!')<"+"/script>"; błędu nie spowoduje.