Bezpieczeństwo
Klient FiveM to program na cudzym komputerze. Loggery eventów i executory Lua to zwykłe, swobodnie dostępne narzędzia, a serwer, który jest bezpieczny tylko dopóki rozmawia z nim własna strona telefonu, nie jest bezpieczny. Oto, co to znaczy w praktyce w tym zasobie, i gdzie leżą reguły.
Wszystko poniżej zostało przejrzane handler po handlerze i wyszło z tego jedenaście rzeczy. Są wymienione na końcu, nie dlatego, że lista jest ciekawa, ale dlatego, że ciekawy jest jej kształt: cztery z nich były tym samym błędem w czterech różnych miejscach.
Potem to samo pytanie zadano w drugą stronę — nie co klient może wysłać, ale w co telefon wierzy na temat tego, co dostaje — i to jest reguła szósta.
Sześć reguł
Dział zatytułowany „Sześć reguł”1. Kim jesteś, pochodzi z serwera, nigdy z wiadomości.
Phone.Handler(name, fn) wywołuje fn(src, data, player), gdzie src to połączenie, a player jest z niego wyszukiwany. Żaden handler nie bierze od klienta identyfikatora, numeru telefonu ani postaci, by zdecydować, czyich danych dotknąć. Odbiorca jest rozwiązywany z numeru telefonu przez bazę danych; nadawcą jest zawsze wywołujący.
Jedyny raz, gdy to zostało złamane, mic_phone:boot był zarejestrowany jako samo Phone.Boot — a drugi argument Phone.Boot to rekord, z którego buduje się telefon, podczas gdy callback przekazuje handlerowi jako jego drugi argument to, co wysłał klient. Każdy mógł poprosić o telefon dowolnej osoby po nazwie. server/tests.lua zawiera test, który wywołuje zarejestrowany callback ze spreparowaną postacią i oblewa, jeśli dostanie z powrotem telefon.
2. Własne eventy frameworka nie są drzwiami.
RegisterNetEvent otwiera nazwę eventu dla sieci dla tego zasobu. ESX wywołuje esx:playerLoaded i esx:playerLogout lokalnie, przez TriggerEvent, i nigdy ich nie otwiera — więc zarejestrowanie ich tutaj jako net eventów sprawiło, że mic_phone przyjmował je od dowolnego klienta, z dowolnym id gracza w środku. To prowadzi wprost do Phone.OnDropped: zakończ rozmowę tego gracza, wyrzuć go z radia, zapomnij jego telefon.
AddEventHandler dla wszystkiego, co framework wywołuje sam. Linter powie Ci coś przeciwnego; jest w błędzie, a posłuchanie go przywraca dziurę.
3. Nic, co wysyła klient, nie jest przekazywane dalej w takiej postaci, w jakiej przyszło.
Wszystko, co zostanie zapisane lub pokazane komuś innemu, jest odbudowywane z pól, które znamy, zamiast być przekazywane w całości: Nearby.Shared dla tego, co jeden telefon przekazuje drugiemu, attachment() dla tego, co niesie wiadomość, żądanie w services:request, element w citylist:save. Pole, którego nikt nie nazwał, nie przeżywa podróży.
4. Adres jest sprawdzany względem hostów, których używa ten serwer.
Photos.AllowedUrl — https, poniżej 600 znaków i host z ServerConfig.Media.Hosts. Przez to przechodzi każdy obraz, który trafia do wiersza bazy danych lub na ekran innego gracza: aparat, udostępnienia w pobliżu, zdjęcia ogłoszeń, załączniki wiadomości, posty i profile społecznościowe.
Bez tego spreparowany klient może umieścić wybrany przez siebie adres przed każdym graczem, który obok przewinie — co jest sposobem na zbieranie adresów wszystkich na serwerze — albo, ponieważ te kolumny to LONGTEXT, przenieść cały obraz wewnątrz samego linku.
5. Wszystko ma rozmiar i tempo.
DB.Fit(value, width) | string, przycięty do swojej kolumny |
DB.Json(value, limit) | tabela, zakodowana, albo nic, jeśli się nie zmieści |
DB.SetJson | 128 KB na blob; największy widziany prawdziwy ma 929 bajtów |
Phone.TooFast | 100 wywołań na 10 sekund, na gracza |
Photos.Chunk | 3 przesyłania w toku, 24 MB, sprzątane po minucie ciszy |
Contacts.Most | 200 |
Calls.MostRecents | 100 |
MOST_ATTACHMENTS | 8 |
Rozmiary mają większe znaczenie, niż się wydaje. Baza danych działa w trybie strict, więc wartość o jeden znak szersza niż jej kolumna nie jest przycinana — wstawienie się nie udaje, w środku tego, co ten handler jeszcze robił. W przelewie ta linijka jest po przesunięciu pieniędzy.
6. To, co napisał inny gracz, jest tekstem, a strona jest zbudowana tak, by nie mogło przestać nim być.
Pięć powyższych dotyczy tego, co dociera do serwera. Ta dotyczy tego, co dociera do telefonu, i jest groźniejszym kierunkiem: strona, która rysuje cudzy profil, to ta sama strona, która może wywołać każdy handler jako swój właściciel. Tag, który się tam wykona, może czytać wiadomości tego gracza i przesuwać jego pieniądze. Nie potrzebuje executora — potrzebuje nazwy.
Telefon rysuje się za pomocą template literali, więc każda wartość jest znacznikami, chyba że coś ją escapuje. escapeHtml w core/app.js jest tym czymś, a rpEsc w aplikacjach miejskich to ta sama funkcja pod lokalną nazwą. Dwie połowy:
Escapuj w miejscu zapisu. Każda interpolacja niosąca czyjeś słowa — nazwa, handle, podpis, notatka, dzielnica, adres — przechodzi przez jedną z tych dwóch. Sprawdzono to, parsując każdy template literal w html/, a nie czytając je, bo jest ich 2918.
I nie polegaj na tym. html/index.html zawiera Content-Security-Policy bez 'unsafe-inline' w script-src. Nic w tym telefonie nie jest skryptem inline ani handlerem inline — każdy skrypt jest plikiem — więc polityka nic nie kosztuje i sprawia, że onerror, który strona została nakłoniona do zapisania, po prostu się nie wykona. To ta połowa, która nie polega na tym, że następna osoba, która pisze funkcję renderującą, cokolwiek zapamięta. server/web.js dodaje dwie rzeczy, których <meta> nie potrafi powiedzieć: frame-ancestors 'none', ponieważ link do połączonego telefonu to cały sekret, a ten telefon może wysyłać pieniądze, oraz nosniff.
tests/verify-injection.js podaje każdej aplikacji ciąg, który byłby jednoznaczny, gdyby kiedykolwiek przestał być tekstem, a potem przeszukuje wyrenderowaną stronę pod jego kątem.
I to, co serwer wysyła bez proszenia. shared/config/*.lua to shared_scripts: każda linijka z nich trafia do klienta każdego gracza i może zostać odczytana z gry executorem. Config.Web przechowywał nazwę, pod którą ten serwer odpowiada, adres, na który wskazuje jego QR, i — dwukrotnie — adres e-mail jego właściciela, z czego żadne nie było nigdy czytane przez klienta. Teraz żyją w shared/server_config.lua, który fxmanifest.lua wymienia w server_scripts i który miał już obok pusty przykład do dostarczenia. Plik configu nie jest prywatnym miejscem, a ustawienia opisujące ten serwer to te, które trzeba z niego wyłączyć.
Gdzie łatwo się pomylić
Dział zatytułowany „Gdzie łatwo się pomylić”Ostrożna gałąź i ta obok niej. Cztery z jedenastu były tym samym błędem: funkcja, która obsłużyła przypadek, o którym ktoś pomyślał, i przepuściła drugi. data: było przesyłane, a url nie, w czterech osobnych miejscach, każde napisane w innym czasie przez kogoś, kto trzy linijki wyżej był właśnie ostrożny. Jeśli dodajesz gałąź do któregokolwiek z nich, zapytaj, co robi ta druga.
Hojny limit to nadal limit. Lista kontaktów czeka na zapytanie na każdy kontakt, a wstawienie zajmuje około jednej trzeciej sekundy na serwerze, na którym to napisano: pięćset kontaktów to prawie trzy minuty bazy danych zajętej przez jedno żądanie. Liczba nie była zła dlatego, że była duża — była zła dlatego, że nikt nie pomnożył jej przez cokolwiek.
Reguły żyją w jednej funkcji, a nie w każdym handlerze. Wszystko w tabeli powyżej jest w jednym miejscu. To jedyny sposób, by reguła przetrwała dodanie handlera przez następną osobę.
Sprawdzenie, które kończy się wcześniej, nie jest sprawdzeniem. Photos.AllowedUrl odczytywało host i zatrzymywało się na pierwszym ukośniku — a wszystko po tym ukośniku to część, którą pisze klient. https:// + dozwolony host + /x.jpg" onerror="… przechodziło przez nie, a telefon wstawiał ten ciąg do src elementu img każdemu graczowi, który otworzył ogłoszenie. Reguła była dobra; zapytano o jedną trzecią adresu.
Jedenaście
Dział zatytułowany „Jedenaście”| Co pozwalało zrobić | Gdzie |
|---|---|
| Kończyć rozmowy dowolnego gracza i wyrzucać go z radia, kiedy się chce | esx:playerLogout zarejestrowany jako net event |
| Zapełnić pamięć serwera, aż się przewrócił | przesyłanie bez limitu i bez sprzątacza |
| Przesuwać pieniądze bez śladu i dostać informację, że się nie udało | notatka szersza niż jej kolumna, w trybie strict |
| Umieścić dowolny adres w bibliotece zdjęć innego gracza | udostępnienia w pobliżu |
| Umieścić dowolny adres przed każdym graczem w mieście | zdjęcia ogłoszeń |
| Umieścić dowolny adres przed wszystkimi w czacie | załączniki wiadomości |
| Umieścić dowolny adres przed wszystkimi w aplikacji | posty i profile społecznościowe |
| Wywoływać dowolny handler tak szybko, jak się dało wysyłać | brak limitu tempa na ścieżce w grze |
| Zapisywać tyle, ile się chciało, wielokrotnie | ustawienia, miejsca, albumy, preferencje |
| Dodawać cokolwiek do żądania wychodzącego | time wklejony do URL bez escapowania |
| Czytać telefon dowolnej postaci | mic_phone:boot, naprawione wcześniej |
Wszystkie jedenaście ma teraz test w server/tests.lua, wcześniej było ich pięć.
Dwa z nich warto wyodrębnić. Zdjęcia ogłoszeń i posty społecznościowe są osiągane wyłącznie przez Photos.AllowedUrl — więc test tej funkcji mówi, że reguła jest dobra, i nic nie mówi o tym, czy te handlery nadal o nią pytają. the handlers that take a photo still ask whether it is one wchodzi drzwiami, tak jak robi to połączony telefon, i odczytuje odmowę: po wycofaniu sprawdzenia spreparowany adres jest akceptowany, ogłoszenie jest zapisywane i rozgłaszane do każdego telefonu w mieście, post jest zapisywany i pokazywany wszystkim w aplikacji, a profil odpowiada
without the picture that was not an address — got https://<a host we allow>/x.jpg" onerror="alert(1)co jest ciągiem, który trafiłby do img. Udany przebieg niczego nie zapisuje; sprzątanie obok jest dla przebiegu nieudanego, czyli tego, w którym to ma znaczenie.
Test eventu frameworka jest tym osobliwym: nie ma sposobu, by zapytać runtime, które nazwy eventów zasób otworzył dla sieci, więc czyta bridge/framework/server.lua i szuka słów. Słabszy niż reszta, a mimo to jest — błąd, przed którym chroni, to taki, który linter aktywnie rekomenduje.
I pięć idących w drugą stronę
Dział zatytułowany „I pięć idących w drugą stronę”| Co pozwalało zrobić | Gdzie |
|---|---|
| Uruchomić kod na telefonie każdego, kto spojrzał na ich profil | nazwa, handle, dzielnica, praca lub zainteresowanie wklejone w znaczniki |
| Uruchomić kod na telefonie każdego, kto otworzył ogłoszenie | adres zdjęcia wklejony w src oraz sprawdzenie adresu, które dopuszczało cudzysłów |
| Uruchomić kod na telefonie każdego, kto czytał rachunek, garaż lub firmę | wystawcy, tytuły, tablice, lokalizacje i szczegóły firm wklejone surowo |
| Sprawić, by cokolwiek z powyższych faktycznie się wykonało | brak jakiejkolwiek polityki treści na stronie |
| Umieścić telefon wewnątrz strony innej witryny | brak frame-ancestors z drzwi webowych |
Pierwsze trzy to po jednym błędzie; ostatnie dwa to powód, dla którego pierwsze trzy miały znaczenie. Wszystkie pięć jest pokrytych przez tests/verify-injection.js, który sprawdza stronę, jaką zbudowałaby przeglądarka, a nie ciągi, które do niej trafiły.
Oba testy najpierw zaobserwowano jako oblewające, po wycofaniu poprawki: ten od adresów wymienia swoje trzy brakujące odmowy, a ten od injection zlicza cztery elementy, które uciekły ze swojego atrybutu — i nic nie mówi o tym, by któryś się wykonał, bo wtedy była już też polityka.