innerHTML, czyli najbardziej banalna furtka do XSS

Bardzo często popełnianym błędem, na który zwracam uwagę praktycznie w każdym kolejnym Weekly JavaScript Challenge, jest niewłaściwe wykorzystywanie innerHTML. Ten wpis chciałem poświęcić tylko i wyłącznie tej właściwości oraz zagrożeniom, które płyną z jej nieprawidłowego wykorzystania. Będę analizował konkretny przykład kodu z nadesłanego rozwiązania i pokażę jak przeprowadzić prosty atak na ten kod. Zapraszam!

XSS

Atak, który tutaj pokażę nazywa się XSS. Odmian XSS jest kilka, a sposobów na przeprowadzenie takich ataków niezliczone mnóstwo. Skupmy się więc na najprostszym przykładzie. Typowym dowodem na to, że podatność na XSS istnieje, jest wyświetlenie na stronie alerta z napisem “1”. Oznacza to, że możliwe jest wykonanie dowolnego kodu JavaScript, a stąd już bliska droga np. do tego, aby wykraść zawartość cookies lub token sesji i móc podszyć się pod danego użytkownika.

Po pierwsze brak zaufania

Kiedy chodzi o bezpieczeństwo aplikacji internetowych bardzo łatwo jest popełnić jakiś prosty błąd, który sprawi, że cała aplikacja będzie podatna na różnego rodzaju ataki. Przy tworzeniu bezpieczniejszego oprogramowania bardzo pomaga podążanie za jedną prostą zasadą: Nie ufaj temu, nad czym nie masz kontroli.

Oczywiście przestrzeganie jednej prostej zasady nie sprawia, że tworzony przez nas kod jest całkowicie bezpieczny. Bezpieczeństwo aplikacji jest tematem niezwykle szerokim, a dosłownie każdego dnia powstają nowe ataki. Czuję, że w tym wpisie nie dotykam tutaj nawet czubka góry lodowej. Każdemu profesjonaliście polecam zgłębiać ten temat na bieżąco.

Nie ufaj nikomu

I teraz kilka pytań kontrolnych. Którym z tych źródeł danych możemy zaufać?

  • Zawartość pól uzupełnionych przez użytkownika w formularzu na stronie.
  • Odpowiedzi zewnętrznych serwisów (tzw. third party API) przykładowo Giphy albo Facebook.
  • Odpowiedzi z API aplikacji, którą tworzymy.

Jakie są prawidłowe odpowiedzi? Niektórzy mogą argumentować (prawdopodobnie słusznie!), że jedyne akceptowalne odpowiedzi to: Nie, nie i jeszcze raz nie. Nieco paranoiczne, ale pewnie najbezpieczniejsze. Czy to jednak oznacza, że mamy w ogóle nie korzystać z zewnętrznych usług? Absolutnie nie! Jak najbardziej możemy to robić, jednak treści zwracane przez wszelkie API należy zawsze traktować jako niezaufane i odpowiednio sprawdzać, filtrować i walidować. Dlaczego? Dokładniej odpowiedziałem na to pytanie w dalszej części wpisu.

Jak to się ma do innerHTML?

Właśnie. Jak wykorzystywanie właściwości innerHTML ma się w ogóle do tych trzech pytań? Muszę przyznać, że nie najlepiej. No ale przejdźmy do konkretów. Czcze gadanie pewnie nie zda się na zbyt wiele, więc przeanalizujmy szybko pewien przykład kodu. Jest to fragment autentycznego rozwiązania wrzuconego na organizowane przeze mnie Weekly JavaScript Challenge. Muszę tutaj zaznaczyć: Niestety bardzo wiele rozwiązań korzystało z innerHTML w sposób zagrażający bezpieczeństwu.

Na przykładzie…

Poprzedni Weekly JavaScript Challenge polegał na stworzeniu prostej aplikacji umożliwiającej wyszukiwanie gifów przy pomocy Giphy.com. Po otrzymaniu odpowiedzi z API, konieczne było stworzenie kilku elementów <video> i wypełnienie atrybutów odpowiednimi wartościami. W jednym z rozwiązań było to realizowane w poniższy sposób:

for (var i = 0; i < response.length; ++i) {  
    var src = response[i].images.original.mp4;
    element.innerHTML += '<video src="' + src + '"></video>';
}

Czyli krótko mówiąc: Bierzemy odpowiedź z API i wkładamy ją do kodu HTML na stronie. Bez sprawdzenia co w tej odpowiedzi jest i bez jakiejkolwiek walidacji.

Ale przecież…

Wiele osób teraz prawdopodobnie podrapie się po głowie i powie: Ale przecież ja ufam API Giphy. Gdybym mu nie ufał to przecież aplikacja nie miałaby sensu!

Oczywiście, jest to słuszna uwaga. Po części. Zwracam jednak uwagę na trzy ważne rzeczy:

Po co się narażać?

innerHTML jest tutaj absolutnie niepotrzebny; ten sam cel można osiągnąć w lepszy sposób. Nie ma sensu korzystać z metody, która jest potencjalnie niebezpieczna jeśli istnieje bezpieczniejsza alternatywa. Dobre nawyki warto wyrabiać na każdym kroku.

Czy na pewno Giphy to Giphy?

Oryginalnej odpowiedzi z API Giphy można raczej zaufać. Jednak nigdy nie mamy pewności, czy przypadkiem samo Giphy nie stało się ofiarą ataku. Co jeśli ktoś podmieni odpowiedzi z API Giphy na inne, niebezpieczne? Jest to absolutnie możliwe i zdarzało się w przypadku innych serwisów!

Czy użytkownik jest bezpieczny?

Jaką mamy pewność, że użytkownik naprawdę komunikuje się z API Giphy? Absolutnie żadnej. Nie wiemy, czy dana osoba sama nie padła ofiarą jakiegoś ataku – a możliwości jest całe mnóstwo.

Najprostszy przykład: Wystarczy dodać jedną linijkę do pliku /etc/hosts, aby cała komunikacja z Giphy została przekierowana na zupełnie inny serwer! Podobnych, bardziej wyszukanych ataków można wymienić znacznie więcej, choćby DNS spoofing i inne man in the middle. Zdarza się Wam korzystać z niezabezpieczonych sieci wifi w kawiarniach? Właśnie.

Przeprowadźmy atak!

Rozwińmy nieco poprzedni podpunkt i przeprowadźmy prosty atak XSS na takiego użytkownika. Zwyczajowo spróbujemy doprowadzić do sytuacji, w której wykona się kod alert(1). Do dzieła!

Eksploatujemy innerHTML

Zastanówmy się jednak jaki dokładnie kod musiałby się znaleźć w odpowiedzi z API, aby atak się powiódł. Przywołajmy fragment kodu z innerHTML:

element.innerHTML += '<video src="' + src + '"></video>';  

Czy wystarczy, że jako src wstawimy tag <script>?

" <script> alert(1); </script> "

Niestety nie. A w zasadzie stety, bo to bardzo dobrze 😉 Zgodnie ze specyfikacją HTML5, kod w tagu <script> umieszczonym na stronie przy pomocy innerHTML nie powinien zostać wykonany. Wspominałem już o tym we wpisie podsumowującym 1. Weekly JavaScript Challenge i napisałem wtedy też, że istnieją inne metody wykonania arbitralnego kodu na stronie. Przykładowo możemy skorzystać z atrybutu onerror i w nim wywołać kod, który nas interesuje:

<video src="nieistnieje" onerror="alert(1)">  

Wstawienie takiego fragmentu HTML przy pomocy innerHTML spowoduje uruchomienie kodu i wyświetlenie komunikatu z napisem “1”. Wykorzystajmy to!

Fałszywe API

Załóżmy, że udało nam się przekierować całą komunikację użytkownika na adres IP naszej maszyny (no, ale tego jak to zrobić nie będę tutaj opisywał 😉 ). Na potrzeby przetestowania kodu możecie po prostu dodać taką linijkę do /etc/hosts:

127.0.0.1 api.giphy.com  

Pod tym IP stawiamy prosty serwer HTTP, przykładowo w node.js:

const http = require('http');

const server = http.createServer(handleRequest);

server.listen(80, () => {  
    console.log("Server listening on: http://localhost");
});

Co musi robić funkcja handleRequest? Po pierwsze ustawić odpowiednie nagłówki pozwalające na CORS. Potem już tylko zwrócić JSON z niebezpieczną zawartością:

function handleRequest(request, response){  
    response.writeHead(200, {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*"
    });

    response.end(JSON.stringify({
        data: [{ images: { original: {
            mp4: 'nieistnieje" onerror="alert(1)'
        } } }]
    }));
}

Teraz, gdy ktoś skorzysta z naszej wyszukiwarki gifów, zamiast rezultatu ujrzy komunikat z napisem “1”. Było trochę zachodu, ale udało nam się dowieść podatności na XSS w aplikacji!

Naprawmy ten kod!

Naprawmy więc oryginalny kod wstawiający elementy video na stronę. Jak zrobić to poprawnie? Najlepiej najpierw użyć funkcji document.createElement, a potem ustawić atrybut src:

for (var i = 0; i < response.length; ++i) {  
    var src = response[i].images.original.mp4;
    var video = document.createElement('video');
    video.src = src;
    element.appendChild(video);
}

Teraz nawet jeśli w odpowiedzi z API znajdzie się potencjalnie niebezpieczny kod to nie zostanie on po prostu dodany do strony – nigdy nie „opuści” atrybutu src elementu <video> i wygenerowany HTML będzie wyglądał jakoś tak:

<video src="nieistnieje&quot; onerror=&quot;alert(1)"></video>  

Podobnie jeśli potrzebujemy dodać na stronę jakiś tekst, korzystanie z innerHTML to proszenie się o kłopot. Lepiej użyć właściwości textContent.

Jak się bronić?

Śmiało mogę powiedzieć, że korzystanie z innerHTML w taki sposób jest najbardziej banalną furtką dla ataków XSS. Zwracam jednak uwagę, jak już wspomniałem na początku artykułu, nawet nie dotknąłem czubka góry lodowej tematu bezpieczeństwa aplikacji internetowych. Możliwości przeprowadzenia ataków jest całe mnóstwo, a każdego dnia odkrywane są nowe podatności!

Dokąd dalej?

Tym wpisem chciałem tylko pokazać i uczulić, że nawet tak prosty błąd może być katastrofalny w skutkach. Jednocześnie zachęcam do zainteresowania się tematem bezpieczeństwa aplikacji internetowych. Materiałów i przykładów w sieci można znaleźć całe mnóstwo.

Na początek od siebie mogę polecić zin, który opublikował jakiś czas temu Sekurak. Jest to świetna skondensowana dawka wiedzy z niskim progiem wejścia, a wszystko to przedstawione na praktycznych przykładach. Mnie bardzo się też podobała gra Alert 1 to win.

Podsumowanie

Z tego artykułu można na pewno wyciągnąć kilka wniosków. Ten najprostszy to: Nie korzystaj z innerHTML jeśli nie masz pewności, że ufasz danym, które chcesz wstawić na stronę (podpowiedź: rzadko masz taki komfort).

Zachęcam do podzielenia się innymi wnioskami w komentarzach.