Dlaczego w Next.js Server Actions konieczna jest autoryzacja wewnątrz każdego ciała funkcji
Przykład analizowanego przypadku przejęcia konta pokazuje, w jaki sposób nieautoryzowane funkcje Next.js Server Actions umożliwiają wykonywanie uprzywilejowanych operacji, oraz gdzie musi znajdować się kontrola autoryzacji, aby temu zapobiec.
Źródła: Unauthenticated Next.js Server Actions Leading to Zero Click Account Takeover
Server Actions w Next.js umożliwiają komponentowi React bezpośrednie wywołanie funkcji serwerowej, bez konieczności użycia trasy API pomiędzy nimi. Ta wygoda eliminuje jedno z miejsc, w których wiele zespołów ma w zwyczaju umieszczać sprawdzanie uprawnień – mianowicie w obsłudze trasy. Gdy to sprawdzenie nie zostaje przeniesione do ciała funkcji wraz z logiką, funkcja staje się dostępna dla każdego, kto potrafi znaleźć jej identyfikator. Poniższy przykład ilustruje dokładnie taką awarię w rzeczywistym portalu subskrypcyjnym, gdzie dwie niezabezpieczone operacje zostały połączone w celu przejęcia konta bez konieczności klikania, a następnie przedstawiono zasady obronne, które mogłyby temu zapobiec.
Jak faktycznie działają Server Actions
W ramach App Routera akcja serwera to funkcja serwera wywoływana bezpośrednio z komponentu. Framework przypisuje każdej akcji 40-znakowy identyfikator heksadecymalny, umieszcza ten identyfikator w paczce klienta i przekształca wywołanie w żądanie POST zawierające nagłówek Next-Action. Kluczowym elementem jest to, że wywołania są kierowane na podstawie tego nagłówka, a nie ścieżki URL, a ciało żądania to po prostu argumenty funkcji przekazywane w kolejności ich wystąpienia.
Ten projekt ma konsekwencję, której łatwo nie zauważyć. Nie istnieje plik routingu do umieszczenia w middleware, więc nie ma naturalnego punktu kontrolnego, w którym mechanizm ochronny na poziomie frameworka zostałby uruchomiony przed twoim kodem. Jeśli sprawdzenie uprawnień nie jest napisane wewnątrz samej funkcji, to w ogóle nie ma żadnej kontroli. Każdy, kto wydobędzie identyfikatory działań z dostarczonego pakietu i odtworzy strukturę argumentów na podstawie skompresowanych miejsc wywołań, może ponownie wysłać żądanie bez dołączonego pliku cookie sesji.
Pierwsze działanie: e-mail do wewnątrz, uprawniona informacja na zewnątrz
Na omawianym portalu żądanie wysłane bez cookie, bez nagłówka autoryzacji oraz bez niczego innego, co mogłoby zidentyfikować nadawcę, dotarło do pierwszego działania. W ciele żądania znajdował się jedynie adres e-mail oraz wartość logiczna.
POST /particuliers/integrale-web-mensuel-2116 HTTP/2
Host: subdomain.target.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Next-Action: 42a24b711d12f695d798b371e38b584c551ebb6a
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
["victim@gmail.com",true]
Odpowiedź zwróciła pełne dane konta należące do tego adresu e-mail.
{
"user_id": "auth0|000000000000000000000001",
"email": "researcher@example.com",
"identities": [
{ "provider": "auth0", "connection": "www-target-com" }
],
"app_metadata": {
"subscriber_number": "900000000",
"custom_user_id": "00000000–0000–4000–8000–000000000000"
}
}
Kształt tego ładunku danych jest wskaźnikiem. Pola takie jak identities i app_metadata nie są dostarczane przez publiczną API autoryzacji dostawcy tożsamości; pochodzą one z jego API zarządzania, interfejsu administracyjnego dostępnego za pomocą tokena typu machine-to-machine, który posiada uprawnienia do czytania i aktualizowania danych użytkowników. Uzyskanie ich z nieautoryzowanej żądania oznacza, że na serwerze znajdował się uprzywilejowany token, który był potajemnie używany przez anonimową funkcję. Oprócz tego, że samo w sobie stanowi problem, odpowiedź przekazała również dwa wartości niezbędne do dalszych działań: identyfikator użytkownika oraz nazwę połączenia.
Druga akcja: ustawienie hasła dowolnego użytkownika
Druga akcja przyjmowała obiekt oraz ciąg znaków. Obiekt zawierał hasło oraz informacje o połączeniu; ciąg znaków stanowił identyfikator użytkownika. Przesłana do konta testowego, nadal bez autoryzacji, wyglądała w ten sposób.
POST / HTTP/2
Host: subdomain.target.com
Next-Action: 89abcdef0123456789abcdef0123456789abcdef
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
[{"password":"<REDACTED>","connection":"www-target-com"},"auth0|000000000000000000000001"]
Prośba niczego nie udowodniła: brak ciasteczka sesyjnego, brak wiedzy o istniejącym hasle, brak tokena służącego do resetu ani potwierdzenia wysłanego e-mailem, a także brak prośby o dodatkowe potwierdzenie. Serwer odpowiedział pomyślnie, przekazując zaktualizowany obiekt użytkownika. Decydującym błędem jest to, skąd pochodzi identyfikator użytkownika – jest to drugi argument dostarczony przez wywołującego, a nie wartość odczytana z sesji lub pola „subject” podpisanego tokena. Funkcja zmienia ten konta, o którym jej powiedziano.
To sama akcja, inne pole – przejęta ścieżka odzyskiwania dostępu
Ponieważ pierwszy argument jest przekazywany bezpośrednio do funkcji aktualizacji użytkownika dostawcy tożsamości, to pole, które tam umieścisz, jest tym, które ulega zmianie. Zastąpienie hasła adresem e-mail spowoduje natomiast aktualizację adresu e-mail konta.
POST / HTTP/2
Host: subscribe.target.com
Next-Action: 89abcdef0123456789abcdef0123456789abcdef
Content-Type: text/plain;charset=UTF-8
Accept: text/x-component
[{"email":"attacker@example.com","connection":"www-target-com"},"auth0|000000000000000000000001"]
Wynik był taki sam – sukces przy braku jakiejkolwiek weryfikacji prawa własności. Ta wersja jest gorsza, niż się na pierwszy rzut oka wydaje: to adres konta służy do wysyłania informacji o zmianie haseł, więc gdy przestanie należeć do prawdziwego właściciela, proces odzyskiwania dostępu już do niego nie prowadzi. Ponieważ hasło jest ustawiane bezpośrednio, a nie pożyczana jest sesja, uzyskany dostęp nie wygasa sam z siebie.
Zasady, których uczy ten przypadek
Akcje serwera to punkty końcowe backendu przybierające postać funkcji, i zasługują one na takie samo podejrzenie. Umieść sprawdzenie uprawnień w każdej akcji, która dotyka uprzywilejowanych danych lub stanu, oraz potwierdź tożsamość żądającego przed wykonaniem jakichkolwiek innych działań. Identyfikację użytkownika wykonującego akcję uzyskuj z sesji lub zweryfikowanego tokena, nigdy z argumentu kontrolowanego przez klienta – w ten sposób żądanie nie może podać identyfikatora ofiary i zmusić serwera do posłuszeństwa. Trzymaj uprzywilejowane tokeny dostawców z dala od ścieżek kodu dostępnych bez autoryzacji. Traktuj identyfikatory akcji jako publiczne, ponieważ trafiają one do przeglądarki; ukrytość nie jest kontrolą dostępu. Każda funkcja, którą klient może nazwać, to funkcja, którą atakujący może wywołać, a jedyną rzeczą stojącą między nimi a twoimi danymi jest sprawdzenie, które napisałeś w ciele żądania.
Literatura pokrewna
- useOptimistic Rollback: Pięć trybów awarii w Next.js Server Actions — Dowiedz się, dlaczego useOptimistic w tajemnicy przywraca interfejs użytkownika, nie wyjaśniając użytkownikom przyczyn awarii, poprzez pięć sprawdzonych trybów awarii Server Action oraz skuteczną metodę naprawy.
- Debugowanie Next.js Server Actions: Błędy przy wdrażaniu, CORS i ograniczenia objętości danych — Praktyczny przewodnik diagnostyczny wyjaśniający, dlaczego Next.js Server Actions i ścieżki API zawodzą w tajemnicy lub wywołują niejasne błędy, wraz z konkretnymi rozwiązaniami dla każdego przypadku.