Güvenlik
Bir FiveM istemcisi, başkasının bilgisayarında çalışan bir programdır. Olay kaydediciler (event logger) ve Lua executor’lar sıradan, serbestçe erişilebilen araçlardır ve yalnızca telefonun kendi sayfası onunla konuştuğu sürece güvenli olan bir sunucu güvenli değildir. Bu, burada pratikte ne anlama geldiği ve kuralların nerede yaşadığıdır.
Aşağıdaki her şey handler handler elden geçirildi ve bundan on bir şey çıktı. Bunlar sonda listelenir; liste ilginç olduğu için değil, şekli ilginç olduğu için: bunlardan dördü dört farklı yerde yapılmış aynı hataydı.
Sonra aynı soru tersinden soruldu — bir istemcinin ne gönderebileceği değil, telefonun kendisine gönderilene ne kadar inandığı — ve bu altıncı kuraldır.
Altı kural
Section titled “Altı kural”1. Kim olduğunuz sunucudan gelir, asla mesajdan değil.
Phone.Handler(name, fn), fn(src, data, player) çağırır; burada src bağlantıdır ve player ondan bulunur. Hiçbir handler, kimin verisine dokunulacağına karar vermek için istemciden bir identifier, telefon numarası ya da karakter almaz. Bir alıcı, veritabanı üzerinden bir telefon numarasından çözülür; bir gönderen her zaman çağıran kişidir.
Bunun bozulduğu tek seferde, mic_phone:boot doğrudan Phone.Boot olarak kaydedilmişti — ve Phone.Boot’un ikinci argümanı bir telefon inşa edilecek kayıttır, bir callback ise handler’a istemcinin gönderdiği her şeyi onun ikinci argümanı olarak verir. Herkes herkesin telefonunu ada göre isteyebilirdi. server/tests.lua içinde, kayıtlı callback’i uydurma bir karakterle çağıran ve bir telefon geri alırsa başarısız olan bir test vardır.
2. Bir framework’ün kendi event’leri kapı değildir.
RegisterNetEvent, bir event adını bu kaynak için ağa açar. ESX, esx:playerLoaded ve esx:playerLogout event’lerini TriggerEvent ile yerel olarak tetikler ve onları asla açmaz — dolayısıyla bunları burada net event olarak kaydetmek, mic_phone’un onları herhangi bir istemciden, içinde herhangi bir oyuncu id’siyle kabul etmesini sağlardı. Bu da Phone.OnDropped içine yürür: o oyuncunun aramasını bitirin, onu telsizinden çıkarın, telefonunu unutun.
Bir framework’ün kendisinin tetiklediği her şey için AddEventHandler. Bir linter size tersini söyleyecektir; yanlıştır ve ona uymak deliği geri koyar.
3. İstemcinin gönderdiği hiçbir şey geldiği gibi iletilmez.
Saklanacak ya da başkasına gösterilecek her şey, bütün olarak aktarılmak yerine bildiğimiz alanlardan yeniden oluşturulur: bir telefonun diğerine verdiği şey için Nearby.Shared, bir mesajın taşıdığı şey için attachment(), services:request içindeki istek, citylist:save içindeki öğe. Kimsenin adını koymadığı bir alan yolculuktan sağ çıkmaz.
4. Bir adres, bu sunucunun kullandığı host’lara karşı kontrol edilir.
Photos.AllowedUrl — https, 600 karakterin altında ve ServerConfig.Media.Hosts içinden bir host. Bir veritabanı satırına ya da başka bir oyuncunun ekranına ulaşan her resim bundan geçer: kamera, yakındaki paylaşımlar, ilan fotoğrafları, mesaj ekleri, sosyal ağ gönderileri ve profiller.
Bu olmadan, uydurma bir istemci yanından geçen her oyuncunun önüne seçtiği bir adres koyabilir — ki bu, bir sunucudaki herkesin adreslerini toplamanın bir yoludur — ya da bu sütunlar LONGTEXT olduğundan, bağlantının içinde koca bir resmi taşıyabilir.
5. Her şeyin bir boyutu ve bir hızı vardır.
DB.Fit(value, width) | sütununa göre kesilmiş bir dize |
DB.Json(value, limit) | kodlanmış bir tablo; sığmıyorsa hiçbir şey |
DB.SetJson | blob başına 128 KB; görülen en büyük gerçek olan 929 bayt |
Phone.TooFast | oyuncu başına 10 saniyede 100 çağrı |
Photos.Chunk | uçuşta 3 yükleme, 24 MB, bir dakikalık sessizlikten sonra süpürülür |
Contacts.Most | 200 |
Calls.MostRecents | 100 |
MOST_ATTACHMENTS | 8 |
Boyutlar göründüğünden daha önemlidir. Veritabanı strict modda çalışır; dolayısıyla sütunundan bir karakter geniş bir değer kırpılmaz — insert, o handler’ın başka ne yapıyor olduğunun ortasında başarısız olur. Bir transferde o satır, para hareket ettikten sonra gelir.
6. Başka bir oyuncunun yazdığı şey metindir ve sayfa, metin olmaktan çıkamayacak şekilde inşa edilmiştir.
Yukarıdaki beşi sunucuya ulaşanla ilgilidir. Bu, telefona ulaşanla ilgilidir ve daha tehlikeli yöndür: başkasının profilini çizen sayfa, her handler’ı sahibi olarak çağırabilen aynı sayfadır. Orada çalışan bir etiket o oyuncunun mesajlarını okuyabilir ve parasını hareket ettirebilir. Bir executor’a ihtiyacı yoktur — bir isme ihtiyacı vardır.
Telefon kendini template literal’larla çizer; dolayısıyla bir şey onu kaçışlamadıkça (escape) her değer markup’tır. core/app.js içindeki escapeHtml o şeydir ve şehir uygulamalarındaki rpEsc yerel adı altındaki aynı fonksiyondur. İki yarı:
Yazıldığı noktada kaçışlayın. Birinin sözlerini taşıyan her enterpolasyon — bir isim, bir handle, bir altyazı, bir not, bir mahalle, bir adres — bu ikisinden birinden geçer. Bu, okuyarak değil, html/ içindeki her template literal’ı ayrıştırarak kontrol edildi, çünkü bunlardan 2.918 tane var.
Ve buna güvenmeyin. html/index.html, script-src içinde 'unsafe-inline' olmayan bir Content-Security-Policy taşır. Bu telefondaki hiçbir şey satır içi (inline) script ya da satır içi handler değildir — her script bir dosyadır — dolayısıyla politika hiçbir şeye mal olmaz ve bir sayfanın yazması için kandırıldığı bir onerror’un basitçe çalışmaması anlamına gelir. Bir sonraki render fonksiyonunu yazan kişinin bir şeyi hatırlamasına güvenmeyen yarısı budur. server/web.js, bir <meta>’nın söyleyemeyeceği iki şeyi ekler: frame-ancestors 'none', çünkü bağlı bir telefona giden bağlantı sırrın tamamıdır ve o telefon para gönderebilir ve nosniff.
tests/verify-injection.js, her uygulamaya metin olmaktan çıkarsa bariz olacak bir dize verir ve sonra render edilen sayfada onu arar.
Ve sunucunun sorulmadan gönderdiği. shared/config/*.lua dosyaları shared_scripts’tir: içlerindeki her satır her oyuncunun istemcisine ulaşır ve bir executor ile oyundan okunabilir. Config.Web, bu sunucunun cevap verdiği adı, QR’ının işaret ettiği adresi ve — iki kez — sahibinin e-posta adresini tutuyordu; bunların hiçbirini bir istemci hiç okumadı. Şimdi shared/server_config.lua içinde yaşıyorlar; fxmanifest.lua onu server_scripts altında listeler ve yanında gönderilecek boş bir örneği zaten vardı. Bir config dosyası özel bir yer değildir ve bu sunucuyu anlatan ayarlar onun dışında tutulması gerekenlerdir.
Yanlış yapmanın kolay olduğu yerler
Section titled “Yanlış yapmanın kolay olduğu yerler”Dikkatli dal ve yanındaki. On birden dördü aynı hataydı: birinin aklındaki durumu ele alan ve diğerini olduğu gibi geçiren bir fonksiyon. data: yükleniyordu ve url yüklenmiyordu, dört ayrı yerde, her biri farklı bir zamanda, üç satır yukarıda az önce dikkatli olmuş biri tarafından yazılmıştı. Bunlardan herhangi birine bir dal eklerseniz, diğer dalın ne yaptığını sorun.
Cömert bir sınır yine de bir sınırdır. Kişi listesi, kişi başına bir sorguyu bekler ve bir insert, bunun yazıldığı sunucuda yaklaşık üçte bir saniye sürer: beş yüz kişi, tek bir istek tarafından tutulan neredeyse üç dakikalık veritabanı demekti. Sayı büyük olduğu için yanlış değildi — kimse onu bir şeyle çarpmadığı için yanlıştı.
Kurallar her handler’da değil, tek bir fonksiyonda yaşar. Yukarıdaki tablodaki her şey tek bir yerdedir. Bir kuralın, bir sonraki kişinin bir handler eklemesinden sağ çıkmasının tek yolu budur.
Erken duran bir kontrol kontrol değildir. Photos.AllowedUrl host’u okudu ve ilk eğik çizgide durdu — ve o eğik çizgiden sonraki her şey bir istemcinin yazdığı kısımdır. https:// + izin verdiğimiz bir host + /x.jpg" onerror="… bunu geçti ve telefon o dizeyi, ilanı açan her oyuncu için bir img’nin src değerine koydu. Kural doğruydu; adresin üçte biri hakkında soruldu.
On bir
Section titled “On bir”| Birine neye izin verdi | Nerede |
|---|---|
| Herhangi bir oyuncunun aramasını bitirmek ve telsizini düşürmek, istediği zaman | esx:playerLogout net event olarak kaydedilmişti |
| Sunucunun belleğini çökene kadar doldurmak | sınırsız ve süpürücüsüz yüklemeler |
| Kayıtsız para hareket ettirmek ve başarısız olduğunun söylenmesi | strict modda, sütunundan geniş bir not |
| Başka bir oyuncunun fotoğraf kitaplığına herhangi bir adres koymak | yakındaki paylaşımlar |
| Şehirdeki her oyuncunun önüne herhangi bir adres koymak | ilan fotoğrafları |
| Bir sohbetteki herkesin önüne herhangi bir adres koymak | mesaj ekleri |
| Bir uygulamadaki herkesin önüne herhangi bir adres koymak | sosyal ağ gönderileri ve profiller |
| Herhangi bir handler’ı gönderebildiği kadar hızlı çağırmak | oyun içi yolda hız sınırı yok |
| İstediği kadar, tekrar tekrar saklamak | ayarlar, yerler, albümler, tercihler |
| Giden bir isteğe istediği her şeyi eklemek | time, kaçışlanmadan bir URL’ye yapıştırılıyordu |
| Herhangi bir karakterin telefonunu okumak | mic_phone:boot, daha önce düzeltildi |
On birinin hepsinin artık server/tests.lua içinde bir testi var; beşten yukarı çıktı.
İkisi ayrıca ele alınmaya değer. İlan fotoğraflarına ve sosyal ağ gönderilerine yalnızca Photos.AllowedUrl üzerinden ulaşılır — yani o fonksiyonun bir testi kuralın doğru olduğunu söyler ve o handler’ların hâlâ ona sorup sormadığı hakkında hiçbir şey söylemez. the handlers that take a photo still ask whether it is one testi, bağlı bir telefonun yaptığı gibi kapıdan girer ve reddi okur: kontrol geri çıkarıldığında uydurma adres kabul edilir, ilan yazılır ve şehirdeki her telefona yayınlanır, gönderi yazılır ve uygulamadaki herkese gösterilir ve profil şu cevabı verir
without the picture that was not an address — got https://<a host we allow>/x.jpg" onerror="alert(1)ki bu, bir img içine giden dizedir. Geçen çalıştırma hiçbir şey yazmaz; yanındaki temizlik, başarısız olan çalıştırma içindir ve önemli olan çalıştırma da odur.
Framework-event testi tuhaf olanıdır: çalışma zamanına bir kaynağın ağa hangi event adlarını açtığını sormanın bir yolu yoktur, bu yüzden bridge/framework/server.lua dosyasını okur ve kelimeleri arar. Diğerlerinden daha zayıf ve yine de orada — koruduğu hata, bir linter’ın aktif olarak önerdiği bir hatadır.
Ve tersi yönde giden beş
Section titled “Ve tersi yönde giden beş”| Birine neye izin verdi | Nerede |
|---|---|
| Profiline bakan herkesin telefonunda kod çalıştırmak | markup’a yapıştırılmış bir isim, handle, mahalle, meslek ya da ilgi alanı |
| Bir ilanı açan herkesin telefonunda kod çalıştırmak | src içine yapıştırılmış bir fotoğraf adresi ve bir tırnağa izin veren adres kontrolü |
| Bir fatura, bir garaj ya da bir işletme okuyan herkesin telefonunda kod çalıştırmak | düzenleyenler, başlıklar, plakalar, konumlar ve işletme bilgileri, ham olarak yapıştırılmış |
| Yukarıdakilerden herhangi birinin gerçekten çalışmasını sağlamak | sayfada hiç içerik politikası yok |
| Telefonu başka bir sitenin sayfasının içine koymak | web kapısından frame-ancestors yok |
İlk üçü birer hatadır; son ikisi ilk üçünün neden önemli olduğunun sebebidir. Beşi de, içine giden dizeleri değil, bir tarayıcının oluşturacağı sayfayı kontrol eden tests/verify-injection.js tarafından kapsanır.
Her iki test de önce düzeltme geri çıkarılarak başarısız olurken izlendi: adres testi eksik olan üç reddini adlandırır ve enjeksiyon testi, niteliğinden kaçan dört öğeyi sayar — ve birinin çalıştığı hakkında hiçbir şey söylemez, çünkü o noktada politika da oradaydı.