Bảo mật ranh giới client-server

*Nội dung này được dịch bằng AI (Beta) và có thể có lỗi. Để xem trang này bằng tiếng Anh, hãy nhấp vào đây.

Bất cứ khi nào một client có thể kích hoạt một hành động trên server, có khả năng xảy ra lạm dụng. Trong khi RemoteEventsRemoteFunctions là những vectơ tấn công phổ biến nhất ở đây, các trường hợp khác như ProximityPrompt cũng có thể bị ảnh hưởng. Phần này sẽ đề cập đến cách bảo mật ranh giới này.

Xác thực đầu vào từ xa

Mọi mảnh dữ liệu được gửi từ một client phải được xác thực bởi server trước khi được sử dụng. Áp dụng các lớp xác thực này dựa trên những gì mà remote thực hiện.

Xác thực ngữ cảnh/permission

Điều này là cần thiết cho các remote ảnh hưởng đến trạng thái trải nghiệm, tiến trình hoặc các người chơi khác. Server phải xác minh xem người chơi có quyền cần thiết để thực hiện yêu cầu hay không. Ví dụ, người chơi có đủ gần để mua một thứ trong cửa hàng không? Họ có chìa khóa cần thiết để mở một cánh cửa không? Nhân vật của họ có còn sống không?

Xác thực là cần thiết cho các remote mà:

  • Cung cấp phần thưởng hoặc sửa đổi tiến trình của người chơi
  • Ảnh hưởng đến các người chơi khác hoặc trạng thái trải nghiệm chia sẻ
  • Thực hiện các hành động có yêu cầu khoảng cách, thời gian hoặc quyền (ví dụ, khoảng cách đến shop)

Xác thực kiểu và cấu trúc

Một cuộc tấn công khác mà những kẻ khai thác có thể thực hiện là gửi các kiểu hợp lệ về mặt kỹ thuật nhưng làm cho chúng cực kỳ lớn, dài hoặc bị tạch. Ví dụ, nếu server phải thực hiện một thao tác tốn kém trên một chuỗi mà tỉ lệ với độ dài, một kẻ khai thác có thể gửi một chuỗi cực kỳ lớn hoặc bị tạch để làm chậm server.

Một cuộc tấn công phổ biến khác mà kẻ khai thác có thể sử dụng liên quan đến việc gửi tables thay cho một Instance. Payload phức tạp có thể bắt chước những gì sẽ là một tham chiếu đối tượng bình thường.

Ví dụ, với một hệ thống cửa hàng trong trải nghiệm nơi dữ liệu vật phẩm như giá cả được lưu trữ trong các đối tượng NumberValue, một kẻ khai thác có thể xoay quanh tất cả các kiểm tra khác bằng cách thực hiện điều sau:

LocalScript in StarterPlayerScripts

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local payload = {
Name = "Ultra Blade",
ClassName = "Folder",
Parent = itemDataFolder,
Price = {
Name = "Price",
ClassName = "NumberValue",
Value = 0, -- Các giá trị âm cũng có thể được sử dụng, dẫn đến việc tặng tiền thay vì lấy đi!
},
}
-- Gửi payload độc hại đến server (sẽ bị từ chối)
print(buyItemEvent:InvokeServer(payload)) -- Xuất "false Invalid item provided"
-- Gửi một vật phẩm thực đến server (điều này sẽ được thực hiện!)
print(buyItemEvent:InvokeServer(itemDatafolder["Real Blade"])) -- Xuất "true" và tiền còn lại nếu mua thành công
Script in ServerScriptStorage

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local function buyItem(player, item)
-- Kiểm tra xem vật phẩm được truyền vào có bị giả mạo không và có trong thư mục ItemData không
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Invalid item provided"
end
-- Server sau đó có thể tiếp tục xử lý việc mua bán dựa trên dòng chảy mẫu bên dưới
end
-- Liên kết "buyItem()" đến callback của remote function
buyItemEvent.OnServerInvoke = buyItem

Xác thực giá trị

Ngoài việc xác thực kiểu và dữ liệu, bạn nên xác thực các giá trị được truyền qua RemoteEventsRemoteFunctions, đảm bảo rằng chúng là hợp lệ và hợp lý trong ngữ cảnh đang được yêu cầu. Điều này là thiết yếu cho các remote xử lý tiền tệ, vật phẩm, đầu vào số hoặc nội dung do người dùng tạo. Bạn phải đảm bảo rằng các giá trị nằm trong các giới hạn mong đợi (ví dụ: số lượng mua lớn hơn 0) và rằng các ID hoặc tên được cung cấp là hợp lệ (ví dụ: một itemId tương ứng với một vật phẩm thực tế trong trò chơi của bạn).

Những lưu ý đặc biệt cho các giá trị số: Cả inf và NaN đều là các kiểu số hợp lệ, nhưng cả hai đều có thể gây ra vấn đề lớn nếu một kẻ khai thác gửi chúng và chúng không được xử lý đúng cách. Sử dụng các hàm như sau để phát hiện và từ chối chúng:


local function isNaN(n: number): boolean
-- NaN không bao giờ bằng chính nó
return n ~= n
end
local function isInf(n: number): boolean
-- Số có thể là -inf hoặc +inf
return math.abs(n) == math.huge
end

Mối nguy hiểm của NaN

Một kẻ khai thác có thể gửi NaN (Không phải số) như một đối số. NaN đặc biệt nguy hiểm vì nó thuộc kiểu "số" nhưng không thể so sánh qua tất cả các phép so sánh tiêu chuẩn, cho phép nó lén lút vượt qua các kiểm tra logic dường như an toàn. Hãy cùng xem một hệ thống giao dịch dễ bị tấn công, nơi một người chơi đưa ra một đề nghị.


-- MÃ SERVER DỄ BỊ TỔN THƯƠNG
local function onCreateTradeOffer(player, offeredGold)
-- 1. KIỂM TRA KIỂU: Điều này pass! typeof(NaN) là "number".
if typeof(offeredGold) ~= "number" then
return "Invalid offer"
end
-- 2. KIỂM TRA PHẠM VI: Điều này bị vượt qua!
-- (NaN < 0) là false. (NaN > 1000000) cũng là false. Kiểm tra không có gì cả.
if offeredGold < 0 or offeredGold > 1000000 then
return "Offer out of range"
end
-- 3. KIỂM TRA KHO: Điều này bị vượt qua!
-- (NaN > player.Gold.Value) là false.
if offeredGold > player.Gold.Value then
return "Not enough gold"
end
-- LỖI: Một đề nghị giao dịch gian lận với gold NaN đã được tạo ra!
createTrade(player, {gold = offeredGold})
return "Trade offer created."
end

Kẻ khai thác đã thành công tạo ra một đề nghị giao dịch mà không có bất kỳ vàng nào, vì mọi kiểm tra đều thất bại một cách im lặng. Hơn nữa, giá trị NaN duy nhất này sẽ giờ đây đầu độc bất kỳ hệ thống nào khác cố gắng sử dụng nó, và thậm chí còn lan truyền tới những người chơi khác qua đề nghị.

Dưới đây là một số ví dụ tổng quát về thiết kế bảo mật cho các tính năng phổ biến trong trò chơi.

Cửa hàng trong trải nghiệm

Hãy xem xét một hệ thống cửa hàng trong trải nghiệm với một giao diện người dùng, chẳng hạn như một menu chọn sản phẩm với một nút "Mua". Khi nút được nhấn, bạn có thể kích hoạt một RemoteFunction giữa client và server để yêu cầu việc mua. Tuy nhiên, điều quan trọng là server, người quản lý trải nghiệm đáng tin cậy nhất, phải xác nhận rằng người dùng có đủ tiền để mua vật phẩm.

Luồng mua: từ client đến server thông qua một RemoteEvent
Luồng mua: từ client đến server thông qua một RemoteFunction

Nhắm vào vũ khí

Các tình huống chiến đấu cần đặc biệt chú ý đến việc xác thực giá trị, đặc biệt là qua việc nhắm và xác thực cú đánh.

Hãy tưởng tượng một trải nghiệm nơi một người chơi có thể bắn một tia laser vào một người chơi khác. Thay vì client thông báo cho server ai là người bị thương, nó nên thông báo cho server vị trí gốc của phát bắn và phần/vị trí mà nó nghĩ là đã trúng. Server có thể xác thực các điều sau:

  • Vị trí mà client báo cáo là bắn từ có gần nhân vật của người chơi trên server không. Lưu ý rằng server và client sẽ có một chút khác biệt do độ trễ, vì vậy cần phải áp dụng thêm độ dung sai.
  • Vị trí mà client báo cáo trúng cách vị trí của phần mà client báo cáo đã trúng, trên server, là hợp lý.
  • Không có trở ngại tĩnh nào giữa vị trí mà client báo cáo bắn từ và vị trí mà client báo cáo bắn đến. Kiểm tra này đảm bảo rằng client không cố gắng bắn xuyên qua các bức tường. Lưu ý rằng việc này chỉ nên kiểm tra hình học tĩnh để tránh các cú bắn hợp lệ bị từ chối do độ trễ.

Ngoài ra, bạn có thể muốn thực hiện thêm các xác thực từ phía server như sau:

  • Theo dõi khi nào người chơi đã bắn vũ khí lần cuối và xác thực để đảm bảo họ không bắn quá nhanh.
  • Theo dõi số lượng đạn của mỗi người chơi trên server và xác nhận rằng người chơi đang bắn có đủ đạn để thực hiện cuộc tấn công bằng vũ khí.
  • Nếu bạn đã triển khai đội hoặc hệ thống chiến đấu "người chơi chống lại bot", hãy xác nhận rằng nhân vật bị trúng là một kẻ thù, không phải đồng đội.
  • Xác nhận rằng người chơi bị trúng còn sống.
  • Lưu trạng thái vũ khí và người chơi trên server và xác nhận rằng người chơi đang bắn không bị chặn bởi một hành động hiện tại như nạp đạn hoặc một trạng thái như chạy nước rút.

Giới hạn tỷ lệ

Bất cứ khi nào logic từ phía server có thể được kích hoạt bởi client (thông qua remotes, các sự kiện Touched, prompts gần, ClickDetectors, v.v.), có khả năng rằng logic này có thể bị spam bởi những kẻ khai thác hoặc thậm chí những người dùng hợp pháp. Giới hạn tỷ lệ kiểm soát tần suất mà những hành động này có thể được thực hiện, ngăn chặn lạm dụng và quá tải hệ thống. Trong khi thực tế (và đôi khi cần thiết) để thực hiện giới hạn tỷ lệ trên client, không bao giờ chỉ dựa vào một giới hạn tỷ lệ từ phía client.

Đối với bất kỳ logic nào từ phía server có thể được kích hoạt bởi một client, hãy luôn xem xét tỷ lệ tối đa mà logic này nên chạy. Nhiều hành động từ phía server phải có tần suất của chúng bị giới hạn để ngăn chặn lạm dụng hoặc thất bại, chẳng hạn như:

  • Gọi API Roblox: Các API backend như DataStoreServiceBadgeService có các giới hạn tỷ lệ tích hợp sẽ khiến các yêu cầu của bạn thất bại nếu vượt quá
  • Các thao tác tốn tài nguyên: Các hành động tiêu tốn tài nguyên máy chủ đáng kể hoặc ảnh hưởng đến các client khác
  • Ví dụ server: nhân bản các mô hình lớn từ ServerStorage, ngay cả khi chúng không bao giờ được nhân bản đến client
  • Ví dụ client: hướng dẫn tất cả các client kết nối cập nhật GUI của họ cùng một lúc
  • Cơ chế có thể bị lợi dụng: Các hành động có thể bị lạm dụng nếu thực hiện nhanh chóng, chẳng hạn như cấp khả năng bất khả xâm phạm, dịch chuyển người chơi, hoặc thưởng tiền

Ví dụ bình chứa mã thông báo

Một phương pháp vững chắc và phổ biến cho việc giới hạn tỷ lệ là thuật toán bình chứa mã thông báo. Hãy tưởng tượng mỗi người dùng có một bình chứa có thể giữ một số lượng mã thông báo nhất định. Để thực hiện một hành động, người dùng phải tiêu một mã thông báo. Bình chứa sẽ tự phục hồi với các mã thông báo mới với tốc độ ổn định. Phương pháp này cho phép các đợt hành động ngắn trong khi vẫn ngăn chặn spam liên tục bằng cách áp đặt tỷ lệ trung bình qua thời gian.


--!strict
-- Module located in ServerScriptService
type UserId = number
type Bucket = {
tokens: number,
last: number,
}
type TokenBucketT = {
capacity: number,
refillPerSecond: number,
buckets: { [UserId]: Bucket },
allow: (self: TokenBucketT, userId: UserId) -> boolean,
}
local TokenBucket = {}
TokenBucket.__index = TokenBucket
-- Tạo một bộ hạn chế cho phép tối đa `capacity` sự kiện, tự phục hồi lại
-- đầy với tốc độ `capacity / windowSeconds` mã thông báo mỗi giây.
function TokenBucket.new(capacity: number, windowSeconds: number): TokenBucketT
assert(capacity >= 1, "capacity must be >= 1")
assert(windowSeconds > 0, "windowSeconds must be > 0")
local self: TokenBucketT = {
capacity = capacity,
refillPerSecond = capacity / windowSeconds,
buckets = {},
}
return (setmetatable(self, TokenBucket) :: any) :: TokenBucketT
end
local function refill(b: Bucket, now: number, cap: number, rate: number)
local elapsed = now - b.last
if elapsed > 0 then
b.tokens = math.min(cap, b.tokens + elapsed * rate)
b.last = now
end
end
-- Trả về false nếu người dùng sẽ vượt qua giới hạn của họ
function TokenBucket:allow(userId: UserId): boolean
local now = time()
local b = self.buckets[userId]
if not b then
b = { tokens = self.capacity, last = now }
self.buckets[userId] = b
else
refill(b, now, self.capacity, self.refillPerSecond)
end
if b.tokens >= 1 then
b.tokens -= 1
return true
end
return false
end
return TokenBucket

Để sử dụng module, trước tiên bạn tạo một thể hiện giới hạn mới với TokenBucket.new(capacity, windowSeconds). capacity là số lượng tối đa yêu cầu một người dùng có thể thực hiện trong một đợt bùng nổ nhanh, và windowSeconds xác định mất bao lâu để đầy lại tất cả các mã thông báo đó. Ví dụ, TokenBucket.new(5, 10) tạo ra một bộ hạn chế cho phép bùng nổ tối đa 5 yêu cầu và chứa đầy một mã thông báo mỗi hai giây (10 giây / 5 mã thông báo).

Trước khi thực hiện logic được bảo vệ, hãy gọi phương thức :allow(userId), từ chối hành động nếu nó trả về false. Cũng là một thực hành tốt để xóa dữ liệu bình chứa của người dùng khi họ rời đi để ngăn ngừa rò rỉ bộ nhớ.

Ví dụ trình kịch bản sau đây cho thấy cách bảo vệ một RemoteEvent được sử dụng cho một hệ thống trò chuyện tùy chỉnh khỏi bị spam bởi người chơi.


-- Ví dụ sử dụng trong một kịch bản server
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local TokenBucket = require(ServerScriptService.TokenBucket)
local ChatRemote = ReplicatedStorage:WaitForChild("ChatRemote") :: RemoteEvent
-- 5 tin nhắn mỗi 10 giây (khả năng 5, phục hồi tại 0.5 mã thông báo/giây)
local chatLimiter = TokenBucket.new(5, 10)
ChatRemote.OnServerEvent:Connect(function(player: Player, message: string)
if not chatLimiter:allow(player.UserId) then
-- Quá nhanh: bỏ yêu cầu hoặc gửi cảnh báo spam
return
end
-- Xử lý tin nhắn (sau khi xác thực khác)
broadcastMessage(player, message)
end)
-- Dọn dẹp để ngăn ngừa rò rỉ bộ nhớ
Players.PlayerRemoving:Connect(function(p: Player)
chatLimiter.buckets[p.UserId] = nil
end)

Bảo mật các thể hiện được kích hoạt từ client

ProximityPrompts, ClickDetectorsDragDetectors không phải là remotes, nhưng một kẻ khai thác có thể kích hoạt các sự kiện của chúng từ bất kỳ khoảng cách nào, vào bất kỳ thời điểm nào, thường bỏ qua các thuộc tính như Enabled hoặc MaxActivationDistance. Những đối tượng này phải được bảo mật với cùng độ chặt chẽ như các remotes.

ProximityPrompt

  • Một kẻ khai thác có thể kích hoạt bất kỳ sự kiện nào ngay cả khi Enabled là false trên server.
  • Enabled, MaxActivationDistance, và RequiresLineOfSight có thể bị thay đổi một cách âm thầm trên client, vì vậy bất kỳ ProximityPrompt nào được nhân bản đều có thể được nhìn thấy và tương tác từ bất kỳ vị trí nào.
  • Server sẽ chấp nhận các sự kiện hold (như PromptButtonHoldBegan) ngay cả khi HoldDuration phía server là 0. Client có thể thao tác HoldDuration để thực hiện các tương tác một cách bất thường nhanh chóng.
  • Chỉ có sự kiện Triggered có kiểm tra khoảng cách phía server. Các sự kiện khác (như PromptButtonHoldBeganTriggerEnded) không có kiểm tra khoảng cách và có thể được gửi tùy ý bởi bất kỳ client nào.

ClickDetector

  • Không có kiểm tra nào trên server cho bất kỳ sự kiện nào.
  • Kẻ khai thác có thể nhân bản bất kỳ sự kiện nào từ bất kỳ khoảng cách nào, ngay cả khi ClickDetector bị vô hiệu hóa (MaxActivationDistance của 0 hoặc DragDetector.Enabled false).
  • Các sự kiện có thể được nhân bản ngay cả khi ClickDetector không được gán cho một cái gì đó có thể nhấp.

DragDetector

  • Đối với các sự kiện kế thừa từ ClickDetector, không có kiểm tra.
  • Đối với các sự kiện kéo, thuộc tính EnabledPermissionPolicy được kiểm tra và tôn trọng bởi server. Tất cả các thuộc tính khác sẽ không được kiểm tra.

Khi một trong những sự kiện này được kích hoạt, script server của bạn nên thực hiện xác thực của riêng mình trước khi thực hiện bất kỳ hành động nào:

  • Kiểm tra xem nó có được kích hoạt không: Đảm bảo rằng thể hiện dự định được bật bằng cách kiểm tra các thuộc tính của nó (như Enabled, HoldDuration, RunLocally, v.v.) trên server. Bước này không cần thiết nếu đối tượng luôn được bật.
  • Kiểm tra trạng thái người chơi: Đảm bảo rằng người chơi có thể thực hiện hành động, dựa trên trạng thái hiện tại của người chơi. Nhân vật của người chơi có ở trong thế giới không? Nhân vật của họ đủ gần với đối tượng không? Người chơi có cần phải sống để tương tác với đối tượng này không? Kiểm tra bất kỳ trạng thái nào khác của người chơi được duy trì bởi trải nghiệm của bạn trên server nếu cần thiết.
  • Áp dụng giới hạn tỷ lệ: Như đã đề cập trong Giới hạn Tỷ lệ, áp dụng các khoảng thời gian làm mới trên client và thực thi giới hạn tỷ lệ trên server để ngăn chặn những sự kiện này bị spam và lạm dụng. Đối với các tương tác mà bạn mong đợi cần một khoảng thời gian tối thiểu, hãy đảm bảo rằng các client không hoàn thành chúng quá nhanh.

Lưu ý về quyền sở hữu mạng: Theo mặc định, nếu bất kỳ đối tượng nào trong số này là con của các phần không neo hoặc một bộ không neo, một kẻ khai thác có thể chiếm quyền sở hữu mạng của các phần cha và di chuyển chúng trực tiếp đến nhân vật của mình, vượt qua các kiểm tra khoảng cách. Đối với các hành động quan trọng, hãy sử dụng các phần đã neo hoặc API quyền sở hữu mạng cùng với các xác thực cần thiết từ phía server.

RemoteEvents và RemoteFunctions

Giới hạn phạm vi và tác động của RemoteFunctionsRemoteEvents. Hãy rất cẩn thận khi tải động bất kỳ tệp nào (ngay cả vật liệu hoặc âm thanh) hoặc chạy mã trải nghiệm (đặc biệt là thông qua require) dựa trên các đối số của các hàm/sự kiện từ xa. Tránh triển khai các remotes cho phép một client chỉ định một đường dẫn tùy ý hoặc tham khảo thể hiện mà server phải xóa hoặc sửa đổi, ngay cả khi các sửa đổi như vậy có vẻ tầm thường. Các remotes có thể thay đổi bất kỳ trạng thái thể hiện tùy ý nào hoặc thực hiện các thay đổi rộng rãi trên cây DataModel thường có thể liên kết với các lỗi khác để ảnh hưởng nghiêm trọng đến trạng thái hoặc logic tổng thể. Kiểm tra không chỉ loại của các đối số thể hiện mà còn lớp và vị trí hoặc cấu trúc mong đợi trong DataModel.

Giao tiếp từ client đến client

Một số remote được thiết kế để cho phép một client kích hoạt hiệu ứng trên các client khác. Điều này xảy ra khi một client gửi một remote đến server, sau đó server sử dụng RemoteEvent:FireAllClients(), RemoteEvent:FireClient(), hoặc các phương pháp tương tự để chuyển tải thông tin. Mô hình này rất nguy hiểm nếu không được bảo mật đúng cách - server phải là một cổng bảo vệ, không chỉ là một thiết bị truyền tải.

Hãy tưởng tượng một kịch bản nơi người chơi có thể triệu hồi một phép thuật sét. Khi được triệu hồi, các người chơi gần đó thấy ánh sáng flash, nghe tiếng động, và cảm nhận sự rung lắc camera.

Một script server dễ bị tấn công có thể trông như sau:

Mã server dễ bị tấn công (script trong ServerScriptStorage)

local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Script server này chuyển tải thông điệp đến mọi người mà không có xác thực
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
castLightningEvent:FireAllClients(strikePosition)
end)
Mã hiệu ứng client

local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Tạo hiệu ứng hình ảnh và âm thanh trên client
local function createLightningEffect(strikePosition)
-- Mã để rung lắc camera
-- Mã để phát âm thanh sét lớn
-- Mã để tạo ra một tia sét hình ảnh
print("Sét đánh tại: " .. tostring(strikePosition))
end
-- Khi server phát sóng sự kiện, client này sẽ chạy hiệu ứng
castLightningEvent.OnClientEvent:Connect(createLightningEffect)

Một kẻ khai thác có thể lạm dụng hệ thống dễ bị tấn công này bằng cách:

  • Spam: Gọi remote hàng trăm lần mỗi giây, gây ra các hiệu ứng liên tục khiến trải nghiệm không thể chơi được
  • Dữ liệu không hợp lệ: Gửi nil, NaN hoặc các loại sai khiến các lỗi kịch bản trên tất cả các client
  • Sử dụng không được phép: Triệu hồi các phép thuật mà họ chưa mở khóa hoặc không có tài nguyên

Server phải là một cổng bảo vệ, không phải là một thiết bị chuyển tiếp. Trước khi phát sóng, nó phải xác thực yêu cầu và áp dụng giới hạn tỷ lệ cho mỗi người chơi. Dưới đây là một ví dụ về một số phương pháp có thể được thực hiện để xác thực yêu cầu.

Mã server bảo mật

local castLightningEvent = game.ReplicatedStorage.CastLightning
local playerCooldowns = {}
local COOLDOWN_TIME = 3
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
-- 1. Xác thực kiểu
if typeof(strikePosition) ~= "Vector3" then
return -- Loại không hợp lệ, từ chối âm thầm
end
-- 2. Kiểm tra NaN (NaN ~= NaN là cách duy nhất để phát hiện nó)
if strikePosition.X ~= strikePosition.X then
return -- Chứa NaN, từ chối
end
-- 3. Ví dụ giới hạn tỷ lệ
local lastCast = playerCooldowns[player] or 0
if tick() - lastCast < COOLDOWN_TIME then
return -- Vẫn trong thời gian làm mới
end
-- 4. Ví dụ kiểm tra quyền
if not player:GetAttribute("HasLightningSpell") then
return -- Người chơi không có phép thuật này
end
-- 5. Ví dụ xác thực khoảng cách
local character = player.Character
local humanoidRootPart = character and character:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then
return
end
local distance = (humanoidRootPart.Position - strikePosition).Magnitude
if distance > 100 then
return -- Ra khỏi khoảng cách
end
-- Tất cả các kiểm tra đã qua - an toàn để phát sóng
playerCooldowns[player] = tick()
castLightningEvent:FireAllClients(player, strikePosition)
end)

Bằng cách thực hiện những kiểm tra này, việc lạm dụng của một kẻ khai thác trở nên khó khăn hơn để phá hoại trải nghiệm của những người chơi khác. Server hoạt động như một rào chắn bảo vệ, đảm bảo rằng chỉ có các hành động hợp lệ, được ủy quyền được phát sóng đến các client khác.

Những cân nhắc đặc biệt

Dưới đây là một số trường hợp bổ sung mà bạn nên xem xét có thể yêu cầu xử lý chuyên biệt.

Manipulation DataStore

Trong các trải nghiệm sử dụng DataStoreService để lưu trữ dữ liệu người chơi, các kẻ khai thác có thể lợi dụng dữ liệu không hợp lệ, điều kiện đua để làm hỏng lưu trữ hoặc nhân đôi vật phẩm trong DataStore. Điều này đặc biệt vấn đề trong các trải nghiệm có giao dịch vật phẩm, thị trường và hệ thống tiền tệ.

Đảm bảo rằng bất kỳ hành động nào được thực hiện thông qua RemoteEvent hoặc RemoteFunction ảnh hưởng đến dữ liệu người chơi với đầu vào từ client đều được vệ sinh dựa trên các điều sau:

  • Giá trị thể hiện không thể được tuần tự hóa vào một DataStore và sẽ thất bại. Sử dụng xác thực kiểu để ngăn chặn điều này.
  • DataStores có giới hạn dữ liệu. Các chuỗi có độ dài tùy ý nên được kiểm tra và/or bị hạn chế để tránh điều này, bên cạnh việc đảm bảo rằng không thể thêm các khóa tùy ý không giới hạn vào các bảng bởi client.
  • Chỉ số bảng không thể là NaN hoặc nil. Lặp qua tất cả các bảng được gửi từ client và xác thực tất cả chỉ số là hợp lệ.
  • DataStores chỉ có thể chấp nhận các ký tự UTF-8 hợp lệ, vì vậy bạn nên làm sạch tất cả các chuỗi được cung cấp bởi client qua utf8.len() để đảm bảo chúng hợp lệ. utf8.len() sẽ trả về chiều dài của một chuỗi, coi các ký tự unicode là một ký tự duy nhất; nếu gặp phải một ký tự UTF-8 không hợp lệ, nó sẽ trả về nil và vị trí của ký tự không hợp lệ. Lưu ý rằng các chuỗi UTF-8 không hợp lệ cũng có thể xuất hiện trong các bảng như các khóa và giá trị.
  • Lợi dụng số vô cực/NaN - Các kẻ khai thác có thể gửi các giá trị như -1/0, 0/0, hoặc math.huge để gây ra tiền tệ vô hạn hoặc làm hỏng các phép tính. Luôn xác thực bằng các phương pháp được mô tả trong mối nguy hiểm của NaN.

Điều kiện đua có thể xảy ra khi người chơi thao tác thời gian giữa các thao tác để nhân đôi vật phẩm hoặc làm hỏng dữ liệu.

Ví dụ lỗi giao dịch: Một người chơi khởi động một giao dịch, gửi vật phẩm của họ cho một người chơi khác, sau đó ngay lập tức rời khỏi trò chơi. Nếu giao dịch hoàn tất nhưng lưu trữ DataStore của họ không thành công do dữ liệu không hợp lệ, họ tham gia lại với các vật phẩm ban đầu trong khi người chơi khác giữ các vật phẩm đã giao dịch - dẫn đến việc nhân đôi.

Chiến lược phòng ngừa:

  • Xác thực tất cả dữ liệu trước bất kỳ thao tác giao dịch nào
  • Sử dụng các mẫu giống như giao dịch nơi tất cả dữ liệu của người chơi được xác thực trước khi cam kết bất kỳ thay đổi nào
  • Thực hiện xử lý lỗi đúng cách mà hoàn tác tất cả các thay đổi nếu bất kỳ phần nào thất bại

MarketplaceService

Đối với các tương tác liên quan đến MarketplaceService, chẳng hạn như thẻ hoặc sản phẩm của nhà phát triển, tất cả việc xác thực mua sắm và cấp phát vật phẩm phải xảy ra trên server. Cụ thể, hãy sử dụng callback ProcessReceipt để xác thực an toàn các biên nhận mua hàng. Không bao giờ tin tưởng một tín hiệu từ phía client, chẳng hạn như PromptProductPurchaseFinished, để xác nhận một giao dịch mà không có xác thực từ server, vì những điều này có thể bị giả mạo. Đảm bảo rằng hàm ProcessReceipt của bạn kiểm tra kỹ lưỡng các chi tiết biên nhận và chỉ cấp phát vật phẩm hoặc tiền sau khi xác nhận một giao dịch hợp lệ với máy chủ Roblox, và chuẩn bị để xử lý các trường hợp mà một sản phẩm đã được cấp phát cho một biên nhận nhất định.

©2026 Roblox Corporation. Roblox, logo Roblox và Powering Imagination là các nhãn hiệu đã đăng ký và chưa đăng ký của chúng tôi tại Hoa Kỳ và các quốc gia khác.