Open redirect to każda strona w Twoim serwisie, która kieruje przeglądarkę odwiedzającego pod adres URL pochodzący z danych użytkownika, bez jego sprawdzenia. Wygląda to na drobiazg. W praktyce pozwala atakującemu rozesłać link phishingowy z Twoją prawdziwą domeną, a w konfiguracji OAuth może doprowadzić do wycieku kodów autoryzacyjnych i tokenów dostępu.
Dlaczego przekierowanie to coś więcej niż błąd kosmetyczny
CWE-601 opisuje tę słabość jako sytuację, w której aplikacja webowa przyjmuje dane kontrolowane przez użytkownika, wskazujące link do zewnętrznej witryny, a następnie używa go w przekierowaniu. Ofiara klika link do Twojej zaufanej strony i trafia na stronę kontrolowaną przez atakującego. Ściąga OWASP pokazuje, jak to wygląda:
http://example.com/example.php?url=http://malicious.example.com
Nazwa serwera w zmodyfikowanym linku jest identyczna jak w oryginalnej witrynie, więc próba phishingu wygląda bardziej wiarygodnie. Jak wygląda ta sprawa od strony użytkownika, opisujemy przy okazji naszego narzędzia do sprawdzania linków.
W przypadku OAuth ta sama wada sięga dalej. RFC 9700 stanowi, że klienci i serwery autoryzacji MUST NOT udostępniać adresów URL, które przekierowują przeglądarkę pod dowolne URI pobrane z parametru zapytania, ponieważ open redirectory mogą umożliwić eksfiltrację kodów autoryzacyjnych i tokenów dostępu.
Pokrewny przypadek to forwardy. Jeśli dane od użytkownika wybierają wewnętrzny cel, jak w function.jsp?fwd=admin.jsp, spreparowany URL może przejść kontrolę dostępu i dotrzeć do funkcji administracyjnej. Aplikacja musi sprawdzić, czy użytkownik ma uprawnienia do docelowego zasobu.
Wyszukiwanie parametrów
Nazwy bywają różne: url, fwd i returnUrl występują w przykładach OWASP, a w Twoim kodzie pojawią się inne. Szukaj wzorca, a nie nazwy.
| Stos | Niebezpieczny wzorzec |
|---|---|
| Java | response.sendRedirect(request.getParameter("url")) |
| PHP | header("Location: " . $redirect_url) z $_GET['url'] |
| C# .NET | Response.Redirect(url) z request.QueryString["url"] |
| Rails | redirect_to params[:url] |
| Java forward | getRequestDispatcher(fwd).forward(...) |
Pierwszy przebieg za pomocą grep daje listę do przejrzenia. Dostosuj ścieżkę i typy plików do swojego repozytorium.
grep -rnE 'sendRedirect\(|Redirect\(|redirect_to params|header::LOCATION' src/
grep -rn -A3 'header(.Location' src/
Drugie polecenie dotyczy PHP. Przeczytaj linie po każdym trafieniu: jeśli po header("Location... nie ma exit, reszta skryptu nadal się wykonuje. Przeglądarka skonfigurowana tak, by ignorować przekierowanie, może wtedy wyświetlić resztę strony.
Zespoły .NET powinny zacząć od akcji logowania. Według ściągi OWASP witryny w ASP.NET MVC 1 i 2 są szczególnie podatne, a jej przykład LogOn z MVC 2 przekierowuje na returnUrl bez żadnej walidacji. Zalecana poprawka to przejście na MVC 3.
Większość trafień będzie niegroźna. Każde przekierowanie, którego argument nie jest literałem tekstowym ani wynikiem wyszukiwania po stronie serwera, zasługuje na jedno pytanie: skąd pochodzi ta wartość? Liczą się ciasteczka, ukryte pola formularzy, nagłówki i zawartość bazy danych. Wiele open redirectów istnieje dlatego, że programista założył, iż ciasteczka lub ukrytego pola nie da się zmodyfikować.
Aby potwierdzić znalezisko, nie poprzestawaj na jednym zwykłym adresie zewnętrznym. Wyślij w parametrze każdą z poniższych wartości i obserwuj nagłówek Location:
http://malicious.example.comhttps://yoursite.example@malicious.example(userinfo:yoursite.examplestaje się nazwą użytkownika)https://yoursite.example.malicious.example(nazwa hosta zaczynająca się od Twojej)https://malicious-yoursite.example(nazwa hosta kończąca się Twoją)
Ściąga OWASP wskazuje userinfo i mylące nazwy hostów jako sposoby obejścia sprawdzania prefiksu i sufiksu. Jeśli któraś z tych wartości przechodzi, choć zwykły URL został zablokowany, kontrola to prawdopodobnie zwykłe porównywanie ciągów znaków. To wniosek, a nie dowód, ale i tak wymień taką kontrolę.
Skanery wychwycą część takich błędów. OWASP Web Security Testing Guide zaznacza, że te narzędzia są ogólne i nie znają kontekstu konkretnej aplikacji, więc każde znalezisko zweryfikuj ręcznie.
Naprawa, w kolejności od najlepszych rozwiązań
- Nie przekierowuj na podstawie danych od użytkownika. Jeśli cel jest na stałe wpisany w kodzie, atakujący nie może go zmienić.
- Mapuj ID na cele po stronie serwera. Użytkownik podaje krótką nazwę lub ID, a serwer wyszukuje pełny URL i odrzuca wszystko inne. Upewnij się, że ID nie da się przeiterować, bo w przeciwnym razie odwiedzający może wyliczyć wszystkie cele.
- Użyj helpera frameworka dla powrotów lokalnych. W ASP.NET Core to
LocalRedirectlubIsLocalUrl, które odrzucają cele spoza serwisu. - Stosuj allowlistę dla celów zewnętrznych. Użyj utrzymywanego parsera URL i porównuj sparsowany schemat, kanoniczny host i efektywny port z jawną listą. Jeśli dozwolone są tylko określone endpointy, ogranicz też ścieżkę.
- Opcjonalnie wymagaj nonce. CWE opisuje nieprzewidywalny nonce generowany przez aplikację przy każdym żądaniu przekierowania, przy czym żądania bez niego są odrzucane. XSS potrafi to obejść, więc nonce uzupełnia poprawkę, ale jej nie zastępuje.
Zasady dla kontroli opartej na allowliście:
- Odrzucaj userinfo i dane niejednoznaczne.
- Przekierowuj, używając zwalidowanego URL, a nie wersji, którą przekształcasz już po walidacji.
- Nigdy nie stosuj surowego dopasowania prefiksu ani sufiksu. Ściąga OWASP przywołuje GitLab jako przypadek, w którym userinfo i mylące nazwy hostów pokonały takie kontrole.
Denylista złych wartości to słaba podstawowa obrona, choć CWE dopuszcza ją do wykrywania ataków. Zamiast tego stosuj zasadę "akceptuj tylko znane, dozwolone wartości".
Jeśli użytkownicy muszą opuszczać Twój serwis, dodatkową warstwę daje strona pośrednia, która wskazuje cel i wymaga kliknięcia. Przy jej budowie unikaj XSS.
Strona OAuth
RFC 9700 wymaga, by serwery autoryzacji porównywały adresy przekierowań klienta z wcześniej zarejestrowanymi metodą dokładnego dopasowania ciągów znaków, z wyjątkiem numerów portów w adresach przekierowań localhost w aplikacjach natywnych. Moja interpretacja, nie treść RFC: dokładne dopasowanie potwierdza, że callback jest tym zarejestrowanym, więc jeśli ten zarejestrowany host udostępnia też open redirector, kod może powędrować dalej. RFC odsyła po szczegóły do sekcji 4.11. Przeczytaj ją, zanim oprzesz się na moim streszczeniu.
Od czego zacząć
- Uruchom powyższe wzorce grep na swoim kodzie i wypisz każde przekierowanie i forward, których cel pochodzi z żądania, ciasteczka lub zapisanej wartości. Najpierw sprawdź obsługę logowania i callbacków OAuth.
- Sprawdź trafienia w PHP pod kątem brakującego
exit, a projekty .NET pod kątem akcji logowania w MVC 1 lub 2. - Przetestuj każde trafienie czterema powyższymi wartościami, w tym formą z userinfo, i zapisz, które z nich kierują przeglądarkę poza Twoją domenę.
- Tam, gdzie to możliwe, zastąp URL-e podawane przez użytkownika mapowaniem ID po stronie serwera, dla adresów powrotu użyj helperów frameworka dopuszczających tylko cele lokalne, a dla nielicznych potrzebnych celów zewnętrznych - allowlisty opartej na sparsowanym URL.
- Przejrzyj rejestracje klientów OAuth pod kątem dokładnych adresów przekierowań i upewnij się, że żaden z Twoich hostów nie udostępnia open redirectora.
- Dodaj test przekierowań do code review i do zakresu testów penetracyjnych, aby ten błąd nie wrócił.
