Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP4 min czytania994 słów

Ź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

  • Pięć zaщит-bezpieczeństwa frontendu, których potrzebuje każda aplikacja React i Next.js — Dlaczego aplikacje React i Next.js w środowisku produkcyjnym używają plików cookie typu HttpOnly, CSP, DOMPurify, nagłóweków bezpieczeństwa oraz reguł NEXT_PUBLIC_, oraz jakie ataki każda z tych metod blokuje.
  • Działania serwera czy obsługa trasy? Przewodnik decyzyjny dla Next.js 16 — Dowiedz się, kiedy mutacja w Next.js 16 powinna znajdować się w działaniu serwera, a kiedy wymaga obsługi trasy, z poprawionymi przykładami dotyczącymi formularzy, błędów, mechanizmu optymizmu oraz webhooków.