Wszechświaty równoległe
Co to, Comandeer się przerzucił na fizykę kwantową? Nic z tych rzeczy, wciąż tylko JavaScript! Niemniej dzisiaj porozmawiamy sobie o… sferach.
Jak rozbić bank?
Udało mi się dzisiaj zepsuć symulator lotto napisany w JS. I choć jego kod znajdował się wewnątrz IIFE, podstawiłem mu własne wyniki – i to bez chamskich zabaw z edytorem kodu w Chrome. Trzon rozwiązania stanowił generator liczb (pseudo)losowych, który używał Math.round
do zwrócenia liczb całkowitych:
( function() {
function getRandomNumber() {
return Math.round( Math.random() * 999999 );
}
for ( let i = 0; i < 10; i++ ) {
console.log( getRandomNumber() ); // 10 losowych liczb
}
}() );
I tutaj na scenę wkraczam ja. Wystarczyło podmienić Math.round
tak, żeby zwracał wybraną przeze mnie wartość, dzięki czemu byłem w stanie za każdym razem trafiać szóstkę:
Math.round = () => 1;
( function() {
function getRandomNumber() {
return Math.round( Math.random() * 999999 );
}
for ( let i = 0; i < 10; i++ ) {
console.log( getRandomNumber() ); // 10 jedynek
}
}() );
Skoro można nadpisać natywne funkcje w JS, to jak się bronić przed tego typu atakami?
Sfery
W standardzie ECMAScript istnieje coś takiego, jak realms (sfery). To nic innego, jak zbiór wszystkich globalnych obiektów (window
, Array
, Math
itd.), czyli gotowe środowisko, w którym nasz kod JS będzie odpalany. Gdy uruchamiamy jakąś stronę w przeglądarce, taka sfera jest tworzona i to w niej uruchamiany jest cały kod. Niemniej ze względu na specyficzne cechy JS-a, na tę sferę możemy wpływać, np. podmieniając konstruktor Array
czy robiąc sztuczkę opisaną powyżej. Zmiana sfery wpływa na wszystkie skrypty uruchamiane wewnątrz niej.
Choć nie jest to regułą, na potrzeby tego artykułu możemy przyjąć, że każde okno (karta) przeglądarki, a także każda ramka (iframe
) posiadają swoje własne sfery. To znaczy, że jeśli otworzymy jedną stronę dwa razy i na jednej z nich zmienimy coś, to na drugiej ta zmiana nie będzie widoczna. W przypadku ramek ma to też znaczenie z powodów bezpieczeństwa – zwłaszcza, gdy dołączamy zasób z zewnętrznej domeny. Nie do pomyślenia jest, żeby zmiana na czyjejś stronie pozwoliła np. wykraść login z formularza logowania Facebooka.
Wszechświaty równoległe
No dobrze: ale jak się to ma do naszego problemu? Jak już wspomniałem, nową sferę dość prosto uzyskać: wystarczy stworzyć ramkę! Zobaczmy zatem, jakby to wyglądało w praktyce:
Math.round = () => 1;
( function() {
let round;
let random;
function getFreshMathMethod( name ) {
return new Promise( ( resolve ) => {
const iframe = document.createElement( 'iframe' );
document.body.appendChild( iframe );
iframe.onload = () => {
resolve( iframe.contentWindow.Math[ name ] );
document.body.removeChild( iframe );
};
} );
}
function getRandomNumber() {
return round( random() * 999999 );
}
function run() {
for ( let i = 0; i < 10; i++ ) {
console.log( getRandomNumber() ); // 10 losowych liczb
}
}
Promise.all( [
getFreshMathMethod( 'round' ),
getFreshMathMethod( 'random' )
] ).then( ( values ) => {
round = values[ 0 ];
random = values[ 1 ];
run();
} );
}() );
Jak widać, wszystko działa, jak należy. Niemniej kod stał się o wiele bardziej skomplikowany. Wszystko dlatego, że obsługa iframe
jest asynchroniczna.
Przyjrzyjmy się metodzie getFreshMathMethod
, bo to ona jest tutaj kluczowa:
function getFreshMathMethod( name ) {
return new Promise( ( resolve ) => { // 1
const iframe = document.createElement( 'iframe' ); // 2
document.body.appendChild( iframe ); // 3
iframe.onload = () => { // 4
resolve( iframe.contentWindow.Math[ name ] ); // 5
document.body.removeChild( iframe ); // 6
};
} );
}
Zwracam Promise
(1), żeby całość móc później ładnie obsłużyć i żeby kod był wolny od callbacków. Wewnątrz tej obiecanki tworzę ramkę (2) i dodaję ją do document.body
(3). Krok ten jest konieczny, bo przeglądarki nie uruchamiają ramek, które są poza DOM (ot, taka optymalizacja). Gdy zawartość tej ramki się wczyta (4; domyślnie zostanie wczytana “strona” about:blank
), odczytujemy wybraną metodę z jej obiektu window
(5). Każda ramka ma swój własny, globalny obiekt, ukryty pod wlasnością contentWindow
elementu iframe
. Tak odczytaną funkcję zwracamy jako wartość obiecanki. Na sam koniec czyścimy po sobie (6).
Tak pobrane funkcje zapisujemy następnie w lokalnych zmiennych round
i random
i wykorzystujemy w generatorze liczb (pseudo)losowych. I tyle! Niestraszne nam nadpisanie Math
całej strony, bo i tak pobierzemy sobie zawsze świeże metody, z nowo utworzonej sfery.
Jak zatem widać, ramki to wszechświaty równoległe do głównej strony, które mają te same zasady “fizyki”, lecz są całkowicie odrębnymi bytami. I czasami warto z tej odrębności skorzystać.
Komentarze
Przejdź do komentarzy bezpośrednio na Githubie.
Dawne komentarze
Ten blog wcześniej korzystał z systemu komentarzy Disqus. Jednakże pożegnaliśmy się i postanowiłem, że zaimportuję do nowej wersji stare komentarze z niego. Cóż, jego system eksportu na wiele nie pozwala…
Nigdy się nie bawiłem ramkami w taki sposób. Chyba można trochę uprościć kod zmieniając `getFreshMathMethod` na `getFreshWindowObject`. Dzięki za ciekawy wpis!
Dzięki za artykuł. Podasz może rzeczywisty przykład wykorzystania?
Rozumiem przedstawione zasady bezpieczeństwa. Jednak zastanawiam się,
czy w całej mojej historii developerskiej musiałem tak mocno zabezpieczać się przed nadpisaniem globalnych funkcji.
Nie wiem czy strony banków nawet powinny tak zabezpieczyć swoją warstwę kliencką.
Chciałbym zobaczyć faktyczny przykład jak obejść jakieś zaimplementowane zasady, że np.
Math.random musi zawsze zwrócić jakąś pseudo losową wartość bo inaczej to stanie się coś złego.
Nie wiem czy wchodzimy w zbytnie zabezpieczanie tej warstwy klienckiej - jednak może się mylę.
@Comandeer:disqus please powiedz, że się mylę.
W sumie faktycznie jest to bardziej ciekawostka niż realne zabezpieczenie – zwłaszcza, że duża część aplikacji wciąż ma backend, który ostatecznie waliduje wszystkie dane.
Niemniej chyba sensownym use case'em mogłyby być wszelkiego rodzaju gry przeglądarkowe, w których tego typu nadpisywanie byłoby równoznaczne z cheatowaniem. Z drugiej strony: to może stanowić fajne zabezpieczenie przed źle napisanymi frameworkami, które zmieniają wbudowane obiekty (na ciebie patrzę, MooTools…). Tylko w tym wypadku może to być armata na muchę.
Dodatkowe zabezpieczenie przy obfuskacji kodu - tworząc nowy realm można pozbyć się wcześniej nałożonych proxy.
Heh, o możliwości zabawy z metodami natywnymi i modyfikacji zarówno Math jak i np. Array.prototype wie chyba każdy programista JS ale szczerze mówiąc nie wpadłbym na to, że ta możliwość tak obniża poziom bezpieczeństwa :)
Można by dyskutować nad faktycznym ryzykiem zaistnienia takiej sytuacji ale tak czy inaczej fajnie, że poruszyłeś ten temat. Warto zdawać sobie sprawę z zagrożeń i samodzielnie podjąć decyzję, czy w danym wypadku to faktycznie istotne czy nie.
Ale wg mnie to tylko po raz kolejny pokazuje jak ważna jest dobra serwerowa walidacja danych pochodzących od klienta i nie mówię tu tylko o formularzach, ale o wszystkich danych (które jak pokazujesz, mogą zostać zmodyfikowane poprzez nadpisanie metody natywnej). A w tym konkretnym przykładzie to w zasadzie pytanie, czy nie lepiej po prostu byłoby wrzucenie algorytmu losującego na serwer i po prostu zwrócenie do klienta gotowych liczb.
Interesting ... ^^
A przy okazji, jako że nie mam gdzie, to spytam tutaj: @Comandeer:disqus jakie jest Twoje zdanie na temat SPA / single page apps? Jesteś zwolennikiem, a może hejterem SPA? :-)
Raczej nie przepadam za SPA, bo często są bardzo źle zrobione. Niemniej jeśli jakieś SPA ma bardzo ładne server-side rendering i wie, że zmiana stanu/widoku = zmiana URL-a, to większość rzeczy działa dobrze i sprawnie. Pytanie tylko brzmi, czy to wciąż jest "czyste" SPA?
Niestety, dla fanów SPA .. ja również nie przepadam za SPA generalnie (a już na pewno nie za boomem typu "kurna, przerabiamy wszystko na SPA, bo w sobote oglądałem zaj....e video tutoriale..." :-))
Dinozaury i konserwy może już tak mają :-P Sa use case'y dla SPA nie ma co, nie zawsze jednak musi być to najlepszy way dla projektu. Czyste spa - nie wiem, ale właśnie "bardzo źle zrobione" spa to chyba ogólna bolączka póki co :-P Zrobione dla sztuki, żeby móc powiedzieć, że Hell yeah, mamy SPA (kij że kod czołg rozjechał...)