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:

text
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

lua
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.

lua
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.

lua
-- 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')
lua
-- 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.

lua
-- ESX
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or xPlayer.job.name ~= 'police' then return end
lua
-- QBCore
local Player = QBCore.Functions.GetPlayer(source)
if not Player or Player.PlayerData.job.name ~= 'police' then return end

Dla akcji admina, sprawdzaj uprawnienie ACE zamiast flagi klienta. Zobacz uprawnienia ACE:

lua
if not IsPlayerAceAllowed(source, 'command.myadmin') then return end

Nagrody 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.
lua
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 jak giveMoney(playerId, amount) pozwala oszustowi na zapłacenie komukolwiek.
  • Nie ujawniaj zdarzeń, których nie potrzebujesz. Zdarzenie zarejestrowane tylko z AddEventHandler, bez RegisterNetEvent, nie może być wyzwolone z klienta.
  • Nie uruchamiaj ciągów klientów jako kodu. Nigdy nie przekazuj wartości klienta do load, ExecuteCommand lub 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 →

Czytaj dalej