ทุกครั้งที่ลูกค้าสามารถกระตุ้นการดำเนินการบนเซิร์ฟเวอร์ มีโอกาสที่จะถูกละเมิด ในขณะที่ RemoteEvents และ RemoteFunctions เป็นแหล่งโจมตีที่พบมากที่สุดในที่นี้ แต่กรณีอื่นๆ เช่น ProximityPrompt ก็มีความเสี่ยงเช่นกัน ส่วนนี้จะครอบคลุมวิธีการรักษาความปลอดภัยขอบเขตนี้
การตรวจสอบข้อมูลระยะไกล
ข้อมูลทุกชิ้นที่ส่งจากลูกค้าจะต้องได้รับการตรวจสอบโดยเซิร์ฟเวอร์ก่อนที่จะถูกนำไปใช้ ใช้ชั้นของการตรวจสอบเหล่านี้ตามสิ่งที่ระยะไกลทำ
การตรวจสอบบริบท/การอนุญาต
สิ่งนี้จำเป็นสำหรับการเข้าถึงระยะไกลที่มีผลกระทบต่อสถานะ ความก้าวหน้า หรือผู้เล่นคนอื่น เซิร์ฟเวอร์ต้องตรวจสอบว่า ผู้เล่นมีสิทธิ์ที่จำเป็นในการทำคำขอหรือไม่ ตัวอย่างเช่น ผู้เล่นอยู่ใกล้กับร้านพอที่จะซื้ออะไรบางอย่างไหม? พวกเขามีกุญแจที่จำเป็นในการเปิดประตูไหม? ตัวละครของพวกเขายังมีชีวิตอยู่ไหม?
การตรวจสอบเป็นสิ่งจำเป็นสำหรับระยะไกลที่:
- มอบรางวัลหรือปรับเปลี่ยนความก้าวหน้าของผู้เล่น
- มีผลกระทบต่อผู้เล่นคนอื่นหรือสถานะประสบการณ์ที่แชร์กัน
- ปฏิบัติกับการกระทำที่มีข้อกำหนดในด้านระยะทาง เวลา หรือการอนุญาต (ตัวอย่างเช่น ระยะทางของร้าน)
การตรวจสอบประเภทและโครงสร้าง
การโจมตีอีกอย่างที่ผู้แสวงหาผลประโยชน์สามารถทำได้คือการส่งประเภทที่ถูกต้องตามเทคนิค แต่ทำให้มีขนาดใหญ่ ยาว หรือผิดรูปมาก ตัวอย่างเช่น หากเซิร์ฟเวอร์ต้องดำเนินการที่มีค่าใช้จ่ายสูงบนสตริงที่มีขนาดตรงกับความยาว ผู้แสวงหาผลประโยชน์อาจส่งสตริงที่มีขนาดใหญ่มากหรือผิดรูปเพื่อลดประสิทธิภาพของเซิร์ฟเวอร์
การโจมตีทั่วไปอีกอย่างที่ผู้แสวงหาผลประโยชน์อาจใช้คือการส่ง tables แทน Instance ข้อมูล payload ที่ซับซ้อนสามารถเลียนแบบสิ่งที่ควรจะเป็นการอ้างอิงวัตถุธรรมดา
ตัวอย่างเช่น หากมีระบบร้านค้าในประสบการณ์ in-experience shop ซึ่งข้อมูลรายการเช่นราคาเก็บไว้ในวัตถุ NumberValue ผู้แสวงหาผลประโยชน์อาจหลีกเลี่ยงการตรวจสอบทั้งหมดโดยการทำดังต่อไปนี้:
LocalScript ใน StarterPlayerScriptslocal 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, -- ค่าลบอาจถูกใช้เช่นกัน ส่งผลให้ให้เงินแทนที่จะหักมัน!},}-- ส่ง payload ที่เป็นอันตรายไปยังเซิร์ฟเวอร์ (จะถูกปฏิเสธ)print(buyItemEvent:InvokeServer(payload)) -- ส่งออก "false Invalid item provided"-- ส่งรายการจริงไปยังเซิร์ฟเวอร์ (จะผ่านไป!)print(buyItemEvent:InvokeServer(itemDatafolder["Real Blade"])) -- ส่งออก "true" และจำนวนเงินคงเหลือหากการซื้อสำเร็จ
Script ใน ServerScriptStorage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local function buyItem(player, item)
-- ตรวจสอบว่ารายการที่ส่งมานั้นไม่ถูกปลอมแปลงและอยู่ในโฟลเดอร์ ItemData
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Invalid item provided"
end
-- เซิร์ฟเวอร์สามารถดำเนินการซื้อได้ตามขั้นตอนตามตัวอย่างด้านล่าง
end
-- ผูก "buyItem()" กับการเรียกกลับของฟังก์ชันระยะไกล
buyItemEvent.OnServerInvoke = buyItem
การตรวจสอบค่า
นอกเหนือจากการตรวจสอบประเภทและข้อมูลแล้ว คุณควรตรวจสอบค่าที่ถูกส่งผ่าน RemoteEvents และ RemoteFunctions โดยมั่นใจว่าค่านั้นถูกต้องและสมเหตุสมผลในบริบทที่ร้องขอ สิ่งนี้เป็นสิ่งสำคัญสำหรับระยะไกลที่จัดการสกุลเงิน รายการ การป้อนข้อมูลเชิงตัวเลข หรือเนื้อหาที่สร้างโดยผู้ใช้ คุณต้องมั่นใจว่าค่านั้นอยู่ในขอบเขตที่คาดหวัง (เช่น จำนวนการซื้อมากกว่าศูนย์) และว่า ID หรือชื่อที่ให้มานั้นถูกต้อง (เช่น itemId ตรงกับรายการจริงในเกมของคุณ)
ข้อควรพิจารณาพิเศษสำหรับค่าสถิติ: ทั้ง inf และ NaN เป็นประเภทตัวเลขที่ถูกต้อง แต่ทั้งคู่สามารถก่อให้เกิดปัญหาหากผู้แสวงหาผลประโยชน์ส่งพวกเขาและไม่ได้จัดการอย่างถูกต้อง ใช้ฟังก์ชันเช่นนี้ในการตรวจจับและปฏิเสธมัน:
local function isNaN(n: number): boolean
-- NaN ไม่เคยเท่ากับตัวมันเอง
return n ~= n
end
local function isInf(n: number): boolean
-- ตัวเลขอาจเป็น -inf หรือ +inf
return math.abs(n) == math.huge
end
อันตรายของ NaN
ผู้แสวงหาผลประโยชน์สามารถส่ง NaN (Not a Number) เป็นอาร์กิวเมนต์ NaN มีความอันตรายเป็นพิเศษเพราะเป็นประเภท "number" แต่ล้มเหลวในการเปรียบเทียบมาตรฐานทั้งหมด อนุญาตให้มันเบี่ยงเบนการตรวจสอบเชิงตรรกะที่ดูเหมือนจะปลอดภัย ดูตัวอย่างระบบการค้าที่ยอมรับว่าสามารถเสนอผู้เล่นได้
-- โค้ดเซิร์ฟเวอร์ที่มีช่องโหว่
local function onCreateTradeOffer(player, offeredGold)
-- 1. การตรวจสอบประเภท: นี่ผ่าน! typeof(NaN) คือ "number".
if typeof(offeredGold) ~= "number" then
return "Invalid offer"
end
-- 2. การตรวจสอบช่วง: นี่ถูกหลีกเลี่ยง!
-- (NaN < 0) เป็นเท็จ (NaN > 1000000) ก็เป็นเท็จเช่นกัน การตรวจสอบไม่มีผล
if offeredGold < 0 or offeredGold > 1000000 then
return "Offer out of range"
end
-- 3. การตรวจสอบคลังสินค้า: นี่ถูกหลีกเลี่ยง!
-- (NaN > player.Gold.Value) เป็นเท็จ.
if offeredGold > player.Gold.Value then
return "Not enough gold"
end
-- ช่องโหว่: มีข้อเสนอการค้าที่ถูกปลอมแปลงโดยมีทอง NaN!
createTrade(player, {gold = offeredGold})
return "Trade offer created."
end
ผู้แสวงหาผลประโยชน์สามารถสร้างข้อเสนอการค้าได้สำเร็จโดยไม่มีทองเพราะการตรวจสอบทั้งหมดล้มเหลวอย่างเงียบ ๆ นอกจากนี้ ค่า NaN เดียวนี้จะเข้ามาทำลายระบบอื่น ๆ ที่พยายามใช้งานมัน และอาจแพร่กระจายไปยังผู้เล่นคนอื่นผ่านข้อเสนอ
ด้านล่างนี้เป็นตัวอย่างระดับสูงที่ครอบคลุมการออกแบบที่ปลอดภัยสำหรับคุณสมบัติทั่วไปในเกม
ระบบร้านค้าในประสบการณ์
พิจารณาระบบร้านค้าในประสบการณ์ที่มีส่วนต่อประสานผู้ใช้ เช่น เมนูการเลือกผลิตภัณฑ์ที่มีปุ่ม "ซื้อ" เมื่อกดปุ่ม คุณสามารถเรียกใช้ RemoteFunction ระหว่างลูกค้าและเซิร์ฟเวอร์เพื่อร้องขอการซื้อ อย่างไรก็ตาม เป็นการสำคัญที่เซิร์ฟเวอร์ซึ่งเป็นผู้จัดการที่เชื่อถือได้ที่สุดของประสบการณ์ ยืนยันว่าผู้ใช้มีเงินเพียงพอในการซื้อรายการ

การตั้งเป้าหมายอาวุธ
สถานการณ์การต่อสู้ต้องการความสนใจเป็นพิเศษในการตรวจสอบค่า โดยเฉพาะในการระบุตัวที่เกิดการยิงและการตรวจสอบการโดน
ลองนึกภาพประสบการณ์ที่ผู้เล่นสามารถยิงลำแสงเลเซอร์ไปยังผู้เล่นคนอื่น แทนที่ลูกค้าจะบอกเซิร์ฟเวอร์ว่าควรทำร้ายใคร ควรแจ้งเซิร์ฟเวอร์ถึงตำแหน่งเริ่มต้นของการยิงและส่วน/ตำแหน่งที่คิดว่าถูกตี เซิร์ฟเวอร์สามารถตรวจสอบได้ตามต่อไปนี้:
- ตำแหน่งที่ลูกค้ารายงานว่ากำลังยิงอยู่นั้นอยู่ใกล้กับตัวละครของผู้เล่นบนเซิร์ฟเวอร์ โปรดทราบว่าเซิร์ฟเวอร์และลูกค้าจะแตกต่างกันเล็กน้อยเนื่องจากความหน่วงเวลา ดังนั้นจะต้องใช้ความทนทานเพิ่มเติม
- ตำแหน่งที่ลูกค้ารายงานว่าถูกตีอยู่นั้นอยู่ใกล้กับตำแหน่งของส่วนที่ลูกค้ารายงานว่าถูกตี บนเซิร์ฟเวอร์
- ไม่มีสิ่งกีดขวางคงที่ระหว่างตำแหน่งที่ลูกค้ารายงานว่ากำลังยิงอยู่และตำแหน่งที่ลูกค้ารายงานว่ากำลังยิงไป สิ่งนี้จะตรวจสอบว่าลูกค้าไม่ได้พยายามยิงผ่านกำแพง โปรดทราบว่าการตรวจสอบนี้ควรตรวจสอบเฉพาะเรขาคณิตที่มีโครงสร้างเท่านั้นเพื่อหลีกเลี่ยงการปฏิเสธการยิงที่ถูกต้องเนื่องจากความหน่วงเวลา
นอกจากนี้ คุณอาจต้องการดำเนินการตรวจสอบเพิ่มเติมตามเซิร์ฟเวอร์ดังนี้:
- ติดตามเมื่อผู้เล่นยิงอาวุธครั้งล่าสุดและตรวจสอบเพื่อให้แน่ใจว่าพวกเขาไม่ได้ยิงเร็วเกินไป
- ติดตามจำนวนกระสุนของแต่ละผู้เล่นบนเซิร์ฟเวอร์และยืนยันว่าผู้เล่นที่กำลังยิงมีจำนวนกระสุนเพียงพอในการทำการโจมตีด้วยอาวุธ
- หากคุณได้ดำเนินการตั้งทีม หรือระบบการต่อสู้ "ผู้เล่นต่อสู้กับบอท" ให้ยืนยันว่าตัวละครที่ถูกตีเป็นศัตรูไม่ใช่เพื่อนร่วมทีม
- ยืนยันว่าผู้เล่นที่ถูกตีมีชีวิตอยู่
- จัดเก็บสถานะของอาวุธและผู้เล่นบนเซิร์ฟเวอร์และยืนยันว่าผู้เล่นที่กำลังยิงไม่ถูกบล็อกโดยการกระทำปัจจุบันเช่นการชาร์จหรือสถานะเช่นการวิ่ง
การจำกัดอัตรา
เมื่อใดก็ตามที่มีการดำเนินการใดๆ ที่อยู่ฝั่งเซิร์ฟเวอร์สามารถถูกกระตุ้นจากลูกค้า (ผ่านระยะไกล อีเวนต์ Touched, การแจ้งเตือนที่อยู่ใกล้, ClickDetectors ฯลฯ) จะมีความเป็นไปได้ว่าโลจิกนี้อาจถูกส่งสัญญาณซ้ำโดยผู้แสวงหาผลประโยชน์หรือแม้แต่ผู้ใช้ที่ถูกต้องตามกฎหมาย การจำกัดอัตราควบคุมความถี่ที่การกระทำเหล่านี้สามารถทำได้ โดยป้องกันการล่วงละเมิดและการโหลดระบบ ในขณะที่มันเป็นเรื่องปกติ (และบางครั้งจำเป็น) ที่จะนำการจำกัดอัตรามาลงมือที่ฝั่งลูกค้า อย่าเห็นด้วยโดยขึ้นอยู่กับการจำกัดอัตราที่ฝั่งลูกค้าเพียงอย่างเดียว
สำหรับตรรกะที่อยู่ฝั่งเซิร์ฟเวอร์ซึ่งสามารถถูกกระตุ้นจากลูกค้า ควรคำนึงถึงอัตราสูงสุดที่โลจิกดังกล่าวควรทำงานอยู่ หลายการกระทำที่อยู่ฝั่งเซิร์ฟเวอร์ต้องมีการจำกัดความถี่เพื่อป้องกันการละเมิดหรือล้มเหลว เช่น:
- การเรียก API ของ Roblox: API ฝั่งหลังเช่น DataStoreService และ BadgeService มีการจำกัดอัตราที่จะทำให้คำขอของคุณล้มเหลวหากเกิน
- การดำเนินการที่ต้องใช้ทรัพยากรคำนวณมาก: การดำเนินการที่ใช้ทรัพยากรของเซิร์ฟเวอร์อย่างมากหรือต้องส่งผลกระทบต่อคลิ้นอื่น
- เซิร์ฟเวอร์ตัวอย่าง: การทำสำเนารูปแบบขนาดใหญ่จาก ServerStorage แม้ว่าจะไม่ถูกส่งไปยังลูกค้า
- ตัวอย่างลูกค้า: การสั่งการให้ลูกค้าทั้งหมดที่เชื่อมต่อมีการอัปเดต GUI พร้อมกัน
- กลไกที่สามารถเสี่ยงได้: การกระทำที่อาจถูกละเมิดหากดำเนินการอย่างรวดเร็ว เช่น การให้ความไม่สามารถโจมตีได้ การโทรไปยังผู้เล่น หรือการทำให้มีเงิน
ตัวอย่างถังโทเค็น
วิธีการที่แข็งแกร่งและใช้กันทั่วไปสำหรับการจำกัดอัตราคือ ถังโทเค็น อัลกอริธึม ลองนึกภาพว่าผู้ใช้แต่ละคนมีถังที่สามารถถือโทเค็นจำนวนหนึ่งได้ การดำเนินการใด ๆ ผู้ใช้ต้องใช้โทเค็น ถังจะเติมโทเค็นใหม่ในอัตราที่สม่ำเสมอ วิธีนี้อนุญาตให้มีการระเบิดการกระทำในช่วงเวลาสั้นๆ เมื่อจำเป็น แต่ป้องกันการสแปมอย่างต่อเนื่องโดยการบังคับให้มีอัตราเฉลี่ยเป็นระยะเวลานาน
--!strict
-- โมดูลตั้งอยู่ใน 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
-- สร้างลิมิเตอร์ที่อนุญาตให้เกิดการกระทำสูงสุด `capacity` สำหรับการเติมเต็มกลับไปเป็นเต็มในอัตรา `capacity / windowSeconds` โทเค็นต่อวินาที
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
-- คืนค่า false หากผู้ใช้จะเกินขีดจำกัดของตน
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
ในการใช้โมดูล คุณต้องสร้างตัวอย่างลิมิเตอร์ใหม่ด้วย TokenBucket.new(capacity, windowSeconds) โดย capacity คือ จำนวนคำขอสูงสุดที่ผู้ใช้สามารถทำได้ในช่วงระยะเวลาสั้น ๆ และ windowSeconds กำหนดว่าใช้เวลานานกี่วินาทีในการเติมโทเค็นทั้งหมด ตัวอย่างเช่น TokenBucket.new(5, 10) สร้างลิมิเตอร์ที่อนุญาตให้มีการระเบิดสูงสุดถึง 5 คำขอและเติมโทเค็นหนึ่งตัวทุกๆ สองวินาที (10 วินาที / 5 โทเค็น)
ก่อนที่จะดำเนินการตรรกะที่ได้รับการป้องกัน ให้เรียกใช้วิธี :allow(userId) โดยปฏิเสธคำขอหากส่งคืน false นอกจากนี้ยังเป็นการปฏิบัติที่ดีที่ควรลบข้อมูลถังของผู้ใช้เมื่อพวกเขาออกไปเพื่อป้องกันการรั่วไหลของหน่วยความจำ
ตัวอย่างสคริปต์ต่อไปนี้แสดงวิธีการป้องกัน RemoteEvent ที่ใช้สำหรับระบบแชทที่กำหนดเองจากการถูกสแปมโดยผู้เล่น
-- ตัวอย่างการใช้งานในสคริปต์เซิร์ฟเวอร์
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 ข้อความต่อ 10 วินาที (ความจุ 5, เติมเงินที่ 0.5 โทเค็นต่อวินาที)
local chatLimiter = TokenBucket.new(5, 10)
ChatRemote.OnServerEvent:Connect(function(player: Player, message: string)
if not chatLimiter:allow(player.UserId) then
-- เร็วเกินไป: คำขอจะถูกปฏิเสธหรือต้องมีการแจ้งเตือนการสแปม
return
end
-- ประมวลผลข้อความ (หลังจากการตรวจสอบอื่น ๆ)
broadcastMessage(player, message)
end)
-- การทำความสะอาดเพื่อป้องกันการรั่วไหลของหน่วยความจำ
Players.PlayerRemoving:Connect(function(p: Player)
chatLimiter.buckets[p.UserId] = nil
end)
การรักษาความปลอดภัยของการสร้างที่เกิดจากลูกค้า
ProximityPrompts, ClickDetectors, และ DragDetectors ไม่ใช่ระยะไกล แต่ผู้แสวงหาผลประโยชน์สามารถกระตุ้นเหตุการณ์ของพวกเขาจากระยะทางใดก็ได้ ในเวลาใดก็ได้ โดยมักจะมองข้ามคุณสมบัติต่างๆ เช่น Enabled หรือ MaxActivationDistance วัตถุเหล่านี้ต้องได้รับการรักษาความปลอดภัยด้วยความเข้มงวดเช่นเดียวกับระยะไกล
ProximityPrompt
- ผู้แสวงหาผลประโยชน์สามารถสร้างเหตุการณ์ใด ๆ ได้แม้ว่าจะ Enabled เป็น false บนเซิร์ฟเวอร์
- Enabled, MaxActivationDistance และ RequiresLineOfSight สามารถถูกเปลี่ยนแปลงโดยเงียบจากลูกค้า ดังนั้น ProximityPrompt ที่ถูกทำซ้ำจะสามารถมองเห็นและโต้ตอบได้จากตำแหน่งใดก็ได้
- เซิร์ฟเวอร์จะยอมรับ hold events (เช่น PromptButtonHoldBegan) แม้ว่าการเซิร์ฟเวอร์จะตั้ง HoldDuration เป็นศูนย์ ลูกค้าสามารถปรับแต่ง HoldDuration เพื่อดำเนินการโต้ตอบได้อย่างไม่เป็นธรรมชาติ
- เฉพาะเหตุการณ์ที่ถูก Triggered มีการตรวจสอบระยะห่างจากฝั่งเซิร์ฟเวอร์ เหตุการณ์อื่นๆ (เช่น PromptButtonHoldBegan และ TriggerEnded) ไม่มีการตรวจสอบระยะทาง และสามารถถูกส่งไปยังลูกค้าตามอำเภอใจ
ClickDetector
- ไม่มีการตรวจสอบใด ๆ บนเซิร์ฟเวอร์สำหรับเหตุการณ์ใด ๆ
- ผู้แสวงหาผลประโยชน์สามารถจัดทำซ้ำเหตุการณ์ใด ๆ ได้จากระยะทางใด ๆ แม้ว่าจะ ClickDetector จะถูกปิด (MaxActivationDistance เป็น 0 หรือ DragDetector.Enabled เป็น false)
- เหตุการณ์สามารถถูกจัดทำซ้ำแม้ว่าวัตถุ ClickDetector จะไม่ได้เป็นวัตถุที่สามารถคลิกได้
DragDetector
- สำหรับเหตุการณ์ที่สืบทอดมาจาก ClickDetector จะไม่มีการตรวจสอบ
- สำหรับเหตุการณ์การลาก คุณสมบัติ Enabled และ PermissionPolicy จะถูกตรวจสอบและเคารพโดยเซิร์ฟเวอร์ ในขณะที่คุณสมบัติอื่น ๆ จะไม่ได้รับการตรวจสอบ
เมื่อหนึ่งในเหตุการณ์เหล่านี้ถูกกระตุ้น สคริปต์เซิร์ฟเวอร์ของคุณควรทำการตรวจสอบของตนเองก่อนที่จะดำเนินการใด ๆ:
- ตรวจสอบว่ามันถูกเปิดใช้งาน: ตรวจสอบให้แน่ใจว่าวัตถุที่ตั้งใจจะเปิดใช้งานโดยการตรวจสอบคุณสมบัติของมัน (เช่น Enabled, HoldDuration, RunLocally เป็นต้น) บนเซิร์ฟเวอร์ ขั้นตอนนี้ไม่จำเป็นหากวัตถุนั้นถูกเปิดใช้งานเสมอ
- ตรวจสอบสถานะของผู้เล่น: ยืนยันว่าผู้เล่นสามารถทำการกระทำนี้ได้ตามสถานะปัจจุบันของผู้เล่น ตัวละครของผู้เล่นอยู่ในโลกหรือไม่? ตัวละครของพวกเขาอยู่ใกล้กับวัตถุหรือไม่? ผู้เล่นควรมีชีวิตอยู่เพื่อโต้ตอบกับวัตถุนั้นหรือไม่? ตรวจสอบสถานะอื่น ๆ ของผู้เล่นที่ถูกเก็บรักษาโดยประสบการณ์ของคุณบนเซิร์ฟเวอร์ตามที่จำเป็น
- ใช้การจำกัดอัตรา: ตามที่ครอบคลุมใน การจำกัดอัตรา ใช้เวลาพักจากฝั่งลูกค้าและบังคับใช้การจำกัดอัตราบนเซิร์ฟเวอร์เพื่อป้องกันไม่ให้เหตุการณ์เหล่านี้ถูกสแปมและละเมิด สำหรับการโต้ตอบที่คาดว่าจะใช้เวลานาน ไม่นำไปสู่การดำเนินการที่รวดเร็วเกินไป
หมายเหตุเกี่ยวกับการเป็นเจ้าของเครือข่าย: โดยค่าเริ่มต้น หากวัตถุใด ๆ เป็นลูกของชิ้นส่วนที่ไม่ถูกทำให้แน่นอนหรือการประกอบที่ไม่ถูกทำให้แน่นอน ผู้แสวงหาผลประโยชน์สามารถเข้าครอบครองเครือข่ายของชิ้นส่วนแม่ และย้ายไปยังตัวละครของตนโดยไม่ผ่านการตรวจสอบระยะทาง สำหรับการกระทำที่สำคัญ ใช้ชิ้นส่วนที่ถูกทำให้แน่นอนหรือ API การเป็นเจ้าของเครือข่ายพร้อมกับการตรวจสอบที่จำเป็นด้านเซิร์ฟเวอร์
RemoteEvents และ RemoteFunctions
จำกัดขอบเขตและผลกระทบของ RemoteFunctions และ RemoteEvents ต้องใช้ความระมัดระวังในความปลอดภัยในการโหลดข้อมูลใดๆ (แม้แต่textures หรือเสียง) โดยอิงจากอาร์กิวเมนต์ของฟังก์ชัน/เหตุการณ์ระยะไกล หลีกเลี่ยงการนำเสนอระยะไกลที่อนุญาตให้ลูกค้าระบุเส้นทางหรือการอ้างอิงที่ผู้เสนอให้ลบหรือปรับเปลี่ยนเซิร์ฟเวอร์ แม้การแก้ไขเช่นนี้จะดูเล็กน้อยก็ตาม ระยะไกลที่สามารถเปลี่ยนแปลงสถานะวัตถุใด ๆ หรือสร้างการเปลี่ยนแปลงที่กว้างขวางต่อโครงสร้างของ DataModel สามารถถูกเชื่อมโยงกับข้อบกพร่องอื่น ๆ ได้ซึ่งอาจส่งผลกระทบต่อสถานะหรือโลจิกโดยรวม ตรวจสอบไม่เพียงแต่ประเภทของอาร์กิวเมนต์วัตถุ แต่ยังรวมถึงคลาสและสถานที่หรือโครงสร้างที่คาดหวังใน DataModel ด้วย
การสื่อสารจากลูกค้าสู่ลูกค้า
ระยะไกลบางตัวถูกออกแบบมาให้อนุญาตให้ลูกค้ารายหนึ่งกระตุ้นผลกระทบต่ออีกลูกค้าหนึ่ง โดยปกติเมื่อผู้เล่นส่งระยะทางไปยังเซิร์ฟเวอร์ ซึ่งต่อมาจะใช้ RemoteEvent:FireAllClients(), RemoteEvent:FireClient() หรือวิธีการคล้ายกันเพื่อส่งข้อมูลต่อไป รูปแบบนี้เป็นอันตรายหากไม่ได้รับการปกป้องอย่างเหมาะสม - เซิร์ฟเวอร์ต้องทำหน้าที่เป็นผู้ควบคุม ไม่ใช่เพียงแค่ตัวส่งผ่าน
ลองนึกถึงสถานการณ์ที่ผู้เล่นสามารถร่ายเวทสายฟ้า เมื่อร่ายเสร็จ ผู้เล่นที่อยู่ใกล้เห็นแสงวาบ ได้ยินเสียง และสัมผัสถึงการสั่นสะเทือนของกล้อง
สคริปต์เซิร์ฟเวอร์ที่มีช่องโหว่อาจมีลักษณะเช่นนี้:
โค้ดเซิร์ฟเวอร์ที่มีช่องโหว่ (สคริปต์ใน ServerScriptStorage)
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- สคริปต์เซิร์ฟเวอร์นี้ส่งต่อข้อความไปยังทุกคนโดยไม่มีการตรวจสอบ
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
castLightningEvent:FireAllClients(strikePosition)
end)
โค้ดเอฟเฟกต์ลูกค้า
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- สร้างเอฟเฟกต์ภาพและเสียงบนลูกค้า
local function createLightningEffect(strikePosition)
-- โค้ดสำหรับการสั่นกล้อง
-- โค้ดสำหรับเล่นเสียงฟ้าผ่า
-- โค้ดสำหรับสร้างสายฟ้า
print("Lightning strikes at: " .. tostring(strikePosition))
end
-- เมื่อเซิร์ฟเวอร์ส่งสัญญาณเหตุการณ์ การตอบสนองของลูกค้าจะทำเอฟเฟกต์
castLightningEvent.OnClientEvent:Connect(createLightningEffect)
ผู้แสวงหาผลประโยชน์สามารถใช้ระบบที่มีช่องโหว่นี้ได้โดย:
- การส่งข้อความซ้ำ: เรียกใช้ระยะทางหลายร้อยครั้งต่อวินาที ทำให้เกิดเอฟเฟกต์ต่อเนื่องที่ทำให้ประสบการณ์ไม่สามารถเล่นได้
- ข้อมูลที่ไม่ถูกต้อง: ส่ง nil, NaN หรือประเภทผิดที่ทำให้เกิดข้อผิดพลาดในสคริปต์ในทุก ๆ ลูกค้า
- การใช้งานโดยไม่ได้รับอนุญาต: ร่ายเวทที่พวกเขายังไม่ได้ปลดล็อกหรือไม่มีทรัพยากร
เซิร์ฟเวอร์ต้องเป็น ผู้ควบคุม ไม่ใช่เพียงแค่ตัวส่งผ่าน ก่อนที่จะส่งต่อ ควรมีการตรวจสอบคำขอและบังคับใช้การจำกัดอัตราต่อผู้เล่น นี่คือตัวอย่างของวิธีการที่สามารถใช้ในการตรวจสอบคำขอ
โค้ดเซิร์ฟเวอร์ที่ปลอดภัย
local castLightningEvent = game.ReplicatedStorage.CastLightning
local playerCooldowns = {}
local COOLDOWN_TIME = 3
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
-- 1. การตรวจสอบประเภท
if typeof(strikePosition) ~= "Vector3" then
return -- ประเภทไม่ถูกต้อง ปฏิเสธอย่างเงียบ ๆ
end
-- 2. ตรวจสอบ NaN (NaN ~= NaN เป็นเพียงวิธีเดียวในการตรวจจับมัน)
if strikePosition.X ~= strikePosition.X then
return -- มี NaN ปฏิเสธ
end
-- 3. ตัวอย่างการจำกัดอัตรา
local lastCast = playerCooldowns[player] or 0
if tick() - lastCast < COOLDOWN_TIME then
return -- ยังอยู่ในช่วง Cooldown
end
-- 4. ตัวอย่างการตรวจสอบสิทธิ์
if not player:GetAttribute("HasLightningSpell") then
return -- ผู้เล่นไม่มีเวทมนตร์นี้
end
-- 5. ตัวอย่างการตรวจสอบช่วง
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 -- นอกช่วง
end
-- ตรวจสอบทั้งหมดผ่าน - ปลอดภัยที่จะส่งต่อ
playerCooldowns[player] = tick()
castLightningEvent:FireAllClients(player, strikePosition)
end)
การนำการตรวจสอบเหล่านี้มาใช้ จะทำให้ยากขึ้นสำหรับผู้แสวงหาผลประโยชน์ในการทำผิดประสบการณ์สำหรับผู้เล่นคนอื่น ๆ เซิร์ฟเวอร์ทำหน้าที่เป็นเกราะป้องกันเพื่อให้แน่ใจว่าเฉพาะการกระทำที่ถูกต้องและได้รับการอนุญาตเท่านั้นที่จะถูกส่งไปยังลูกค้าอื่น ๆ
ข้อควรพิจารณาเพิ่มเติม
ด้านล่างนี้เป็นกรณีบางประการที่คุณควรพิจารณาซึ่งอาจต้องการการจัดการเป็นพิเศษ
การจัดการข้อมูลใน DataStore
ในประสบการณ์ที่ใช้ DataStoreService ในการบันทึกข้อมูลผู้เล่น ผู้แสวงหาผลประโยชน์อาจใช้ประโยชน์จากข้อมูลที่ไม่ถูกต้อง เงื่อนไขการแข่งขันในการทำให้การบันทึกเสียหายหรือซ้ำข้อมูลใน DataStore สิ่งนี้มีปัญหาโดยเฉพาะอย่างยิ่งในประสบการณ์ที่มีการซื้อขายรายการ ตลาด และระบบสกุลเงิน
ตรวจสอบว่าการกระทำใด ๆ ที่ดำเนินการผ่าน RemoteEvent หรือ RemoteFunction ที่มีผลต่อข้อมูลผู้เล่นโดยใช้ข้อมูลจากลูกค้านั้นได้รับการทำให้ปลอดภัยตามสิ่งต่อไปนี้:
- ค่าของวัตถุ ไม่สามารถถูกจัดเก็บลงใน DataStore และจะล้มเหลว ใช้การตรวจสอบประเภทเพื่อป้องกันสิ่งนี้
- DataStores มีขอบเขตข้อมูล สตริงด้วยความยาวตามอำเภอใจควรถูกตรวจสอบและ/หรือจำกัดเพื่อหลีกเลี่ยงสิ่งนี้ พร้อมกับการทำให้แน่ใจว่าจะไม่สามารถเพิ่มกุญแจตามอำเภอใจที่ไร้ขีดจำกัดลงในตารางโดยลูกค้า
- ตัวชี้ในตารางไม่สามารถเป็น NaN หรือ nil ตรวจสอบตารางทั้งหมดที่ถูกส่งโดยลูกค้าและยืนยันว่าตัวชี้ทั้งหมดถูกต้อง
- DataStores สามารถยอมรับเฉพาะตัวอักษรที่ถูกต้อง UTF-8 ดังนั้นคุณควรทำให้สตริงทั้งหมดที่จัดทำโดยลูกค้าเป็นไปตาม utf8.len() เพื่อให้แน่ใจว่าพวกเขาถูกต้อง utf8.len() จะส่งคืนความยาวของสตริง โดยถือว่าตัวอักขระ Unicode เป็นตัวอักขระเดียว; หากมีตัวอักษร UTF-8 ที่ไม่ถูกต้องเข้ามา จะส่งคืน nil และตำแหน่งของตัวอักษรที่ไม่ถูกต้อง โปรดทราบว่าสตริง UTF-8 ที่ไม่ถูกต้องยังสามารถปรากฏในตารางได้ในฐานะกุญแจและค่าด้วย
- การแสวงหาผลประโยชน์ด้วยค่าไม่มีที่สิ้นสุด/NaN - ผู้แสวงหาผลประโยชน์อาจส่งค่าต่าง ๆ เช่น -1/0, 0/0 หรือ math.huge เพื่อทำให้เงินไม่มีที่สิ้นสุดหรือล้มเหลวในการคำนวณ ตรวจสอบเสมอโดยใช้วิธีที่อธิบายไว้ใน อันตรายของ NaN.
เงื่อนไขการแข่งขันสามารถเกิดขึ้นเมื่อผู้เล่น manipulates เวลาระหว่างการดำเนินการเพื่อทำซ้ำรายการหรือทำลายข้อมูล
ตัวอย่างการซื้อขายที่ถูกแสวงหาร: ผู้เล่นเริ่มการซื้อขาย ส่งรายการของตนให้ผู้เล่นคนอื่น แล้วออกจากเกมทันที หากการซื้อขายเสร็จสิ้นแต่การบันทึก DataStore ของพวกเขาล้มเหลวเนื่องจากข้อมูลไม่ถูกต้อง พวกเขาจะเข้าร่วมใหม่ด้วยรายการเดิม ในขณะที่ผู้เล่นคนอื่นเก็บรายการที่ซื้อขายไว้ - ทำให้เกิดการซ้ำซ้อน
กลยุทธ์ป้องกัน:
- ตรวจสอบข้อมูลทั้งหมดก่อนที่จะดำเนินการซื้อต่างๆ
- ใช้รูปแบบการทำงานเช่นธุรกรรม ที่ข้อมูลของผู้เล่นทุกคนได้รับการตรวจสอบก่อนที่จะมีการเปลี่ยนแปลง
- ใช้การจัดการข้อผิดพลาดที่เหมาะสมที่ย้อนกลับการเปลี่ยนแปลงทั้งหมดหากส่วนใดส่วนหนึ่งล้มเหลว
MarketplaceService
สำหรับการทำงานร่วมกับ MarketplaceService เช่น การซื้อผ่าน passes หรือผลิตภัณฑ์ที่พัฒนาแล้ว การตรวจสอบการซื้อต้องเกิดขึ้นบนเซิร์ฟเวอร์โดยเฉพาะ ใช้ ProcessReceipt callback เพื่อยืนยันการซื้ออย่างปลอดภัย อย่าเชื่อใจสัญญาณที่อยู่ฝั่งลูกค้า เช่น PromptProductPurchaseFinished เพื่อยืนยันการซื้อโดยไม่มีการตรวจสอบจากเซิร์ฟเวอร์ เนื่องจากสิ่งเหล่านี้สามารถถูกปลอมแปลงได้ ตรวจสอบให้แน่ใจว่าฟังก์ชั่น ProcessReceipt ของคุณตรวจสอบรายละเอียดใบเสร็จอย่างถี่ถ้วนและมอบไอเทมหรือสกุลเงินเฉพาะหลังจากยืนยันว่าการทำธุรกรรมถูกต้องตามกฎหมายกับเซิร์ฟเวอร์ของ Roblox และเตรียมพร้อมที่จะจัดการกับกรณีที่ผลิตภัณฑ์ถูกมอบให้แล้วสำหรับใบเสร็จที่กำหนด