Bezpieczne zdarzenia serwera FiveM: zatrzymaj oszustów przed nadużywaniem net events
Oszuści wyzwalają zdarzenia serwera z dowolnymi wartościami. Jak validować na serwerze: używaj source, sprawdzaj odległość, pracę i cooldowny, i przechowuj ceny w tabeli serwera.
Skrypt sklepu na wielu serwerach wygląda tak i jest wolnym przyciskiem pieniędzy:
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)Każdy z menu oszusta może wysłać tę linię. Serwer uruchamia handler, bierze ujemną cenę od gracza i szczęśliwie dodaje pieniądze. Ten przewodnik pokazuje, jak pisać zdarzenia serwera, które nie ufają klientowi.
Zasada: klient nie jest zaufany
Wszystko, co przybywa w net event pochodzi z komputera gracza, którym ten gracz kontroluje. Traktuj każdy argument jako wejście użytkownika na stronie internetowej: może być brakujące, zły typ, ujemny, ogromny lub po prostu wymyślony.
Co serwer może ufać:
source, gracz, który wyzwolił zdarzenie. Silnik go ustawia.- Dane, które serwer już posiada: praca gracza, inventorz, pieniądze, pozycja i własne tabele konfiguracyjne.
Co nigdy nie powinno ufać: ilości, ceny, nazwy przedmiotów, id graczy, współrzędne, nazwy prac i flagi „sukcesu" wysłane przez klienta.
Zły przykład
RegisterNetEvent('shop:buy', function(item, count, price)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeMoney(price * count)
xPlayer.addInventoryItem(item, count)
end)Klient wybiera element, liczbę i cenę. Oszust kupuje cokolwiek za nic, lub ujemną liczbę, aby wydrukować pieniądze.
Dobry przykład
Przechowuj ceny na serwerze, validuj typy, sprawdzaj gdzie jest gracz, potem działaj.
local Items = {
water = { price = 5, max = 10 },
bread = { price = 8, max = 10 },
}
local shopCoords = vec3(25.7, -1347.3, 29.5)
local lastBuy = {}
RegisterNetEvent('shop:buy', function(item, count)
local src = source
-- 1. types and ranges
if type(item) ~= 'string' or type(count) ~= 'number' then return end
count = math.floor(count)
local def = Items[item]
if not def or count < 1 or count > def.max then return end
-- 2. cooldown
local now = GetGameTimer()
if lastBuy[src] and now - lastBuy[src] < 1000 then return end
lastBuy[src] = now
-- 3. distance
local ped = GetPlayerPed(src)
if #(GetEntityCoords(ped) - shopCoords) > 5.0 then return end
-- 4. price comes from the server table
local total = def.price * count
local xPlayer = ESX.GetPlayerFromId(src)
if not xPlayer or xPlayer.getMoney() < total then return end
xPlayer.removeMoney(total)
xPlayer.addInventoryItem(item, count)
end)
AddEventHandler('playerDropped', function()
lastBuy[source] = nil
end)GetEntityCoords(GetPlayerPed(source)) na serwerze potrzebuje OneSync włączonego. Jeśli tego jeszcze nie zrobiłeś, zobacz włączenie OneSync.
To samo na QBCore i QBox
Zmienia się tylko wyszukiwanie gracza, pole pracy i wezwanie pieniędzy. Validacja pozostaje taka sama.
-- QBCore
local QBCore = exports['qb-core']:GetCoreObject()
local Player = QBCore.Functions.GetPlayer(src)
if not Player or Player.PlayerData.money.cash < total then return end
Player.Functions.RemoveMoney('cash', total, 'shop-purchase')-- QBox
local Player = exports.qbx_core:GetPlayer(src)
if not Player or Player.PlayerData.money.cash < total then return end
Player.Functions.RemoveMoney('cash', total, 'shop-purchase')Sprawdzenia pracy i uprawnień
Jeśli zdarzenie powinno pracować tylko dla pracy, sprawdź to na serwerze z danych frameworku, nie z argumentu.
-- ESX
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or xPlayer.job.name ~= 'police' then return end-- QBCore
local Player = QBCore.Functions.GetPlayer(source)
if not Player or Player.PlayerData.job.name ~= 'police' then return endDla akcji admina, sprawdzaj uprawnienie ACE zamiast flagi klienta. Zobacz uprawnienia ACE:
if not IsPlayerAceAllowed(source, 'command.myadmin') then return endNagrody potrzebują dowodu po stronie serwera
Zdarzenia takie jak job:finished lub mission:reward są klasycznym celem. Oszust po prostu je odpala w pętli.
- Przechowuj ilość nagrody w tabeli serwera, nigdy w argumentach zdarzenia.
- Pamiętaj na serwerze, że gracz zaczął pracę, i płać tylko jeśli ten stan istnieje. Wyczyść go, gdy płacisz.
- Dodaj minimalny czas między startem i zakończeniem.
- Sprawdzaj, czy gracz jest blisko punktu dostawy, gdy kończy.
local activeJobs = {}
RegisterNetEvent('job:start', function()
activeJobs[source] = GetGameTimer()
end)
RegisterNetEvent('job:finish', function()
local src = source
local startedAt = activeJobs[src]
if not startedAt or GetGameTimer() - startedAt < 30000 then return end
activeJobs[src] = nil
-- pay a fixed amount defined on the server
end)Inne nawyki, które pomagają
- Nigdy nie używaj id gracza klienta. Zawsze używaj
source. Zdarzenie takie jakgiveMoney(playerId, amount)pozwala oszustowi na zapłacenie komukolwiek. - Nie ujawniaj zdarzeń, których nie potrzebujesz. Zdarzenie zarejestrowane tylko z
AddEventHandler, bezRegisterNetEvent, nie może być wyzwolone z klienta. - Nie uruchamiaj ciągów klientów jako kodu. Nigdy nie przekazuj wartości klienta do
load,ExecuteCommandlub ciągu SQL. Używaj parametryzowanych zapytań, jak w przewodniku oxmysql. - Zaloguj dziwne przypadki. Gdy sprawdzenie się nie powiedzie, wydrukuj gracza i nazwę zdarzenia. Wzorzec błędów to jak znajdujesz oszustów. Ban z txAdmin, zobacz moderowanie za pomocą txAdmin.
Ostrzeżenie: ukrycie nazwy zdarzenia, zaciemnienie lub dodanie tokenu po stronie klienta „" go nie chroni. Klient może przeczytać wszystkie z tego. Liczą się tylko kontrole na serwerze.
Lista kontrolna
| Symptom | Naprawa |
|---|---|
| Klient wysyła cenę lub ilość | Przechowuj tabelę cen na serwerze i oblicz całość tam |
| Klient wysyła id gracza | Używaj source zamiast |
| Nazwa przedmiotu pochodzi z klienta | Akceptuj tylko nazwy znalezione w tabeli serwera |
| Ujemne lub ogromne ilości | Sprawdzaj type, math.floor i minimum i maksimum |
| Zdarzenie odpalane z drugiej strony mapy | Porównaj GetEntityCoords(GetPlayerPed(source)) z pozycją docelową |
| Zdarzenie spamowane w pętli | Cooldown na gracza, wyczyszczony w playerDropped |
| Akcja tylko dla pracy | Odczytaj pracę z frameworku na serwerze |
| Zdarzenie nagrody nadużywane | Magazyn stan pracy na serwerze i płać ustaloną ilość raz |
Szybkie odpowiedzi
Czy gracz naprawdę może wyzwolić zdarzenia mojego serwera?
Tak. Każde zdarzenie zarejestrowane z RegisterNetEvent może być wyzwolone przez zmodyfikowanego klienta z dowolnymi argumentami. Serwer nie może odróżnić rzeczywistego wezwania skryptu od sfałszowanego, więc musi validować wszystko.
Czy bezpieczne jest używanie id gracza wysłanego przez klienta?
Nie. Na serwerze source jest ustawiany przez silnik i nie może być sfałszowany. Id gracza, imię lub identyfikator wysłany jako argument mogą być, więc nigdy nie używaj go do decydowania, kto dostaje pieniądze lub karę.
Czy potrzebuję antycheat'a, jeśli validuję moje zdarzenia?
Validacja jest bazą i zatrzymuje większość nadużyć twoich własnych skryptów. Antycheat dodaje wykrywanie dla rzeczy, których zdarzenia nie mogą widzieć, takie jak wstrzyknięte menu. Zobacz [FiveM anticheat basics](/blog/fivem-anticheat-basics).
Skrypty bez tego problemu
Shop CreatorZbuduj sklep w niecałą minutę — właściciele, pracownicy, sejfy i napady w zestawie.Zobacz skrypt →
Item Creator V2Twórz używalne przedmioty z animacjami, propami, efektami i nie tylko — bez pisania kodu.Zobacz skrypt →
Advanced BoostingBoosting pojazdów z tabletu: kontrakty od klasy D do S+, ekipy i kolejka na żywo.Zobacz skrypt →