การรักษาความปลอดภัยของขอบเขตลูกค้า-เซิร์ฟเวอร์

*เนื้อหานี้แปลโดยใช้ AI (เวอร์ชัน Beta) และอาจมีข้อผิดพลาด หากต้องการดูหน้านี้เป็นภาษาอังกฤษ ให้คลิกที่นี่

ทุกครั้งที่ลูกค้าสามารถกระตุ้นการดำเนินการบนเซิร์ฟเวอร์ มีโอกาสที่จะถูกละเมิด ในขณะที่ RemoteEvents และ RemoteFunctions เป็นแหล่งโจมตีที่พบมากที่สุดในที่นี้ แต่กรณีอื่นๆ เช่น ProximityPrompt ก็มีความเสี่ยงเช่นกัน ส่วนนี้จะครอบคลุมวิธีการรักษาความปลอดภัยขอบเขตนี้

การตรวจสอบข้อมูลระยะไกล

ข้อมูลทุกชิ้นที่ส่งจากลูกค้าจะต้องได้รับการตรวจสอบโดยเซิร์ฟเวอร์ก่อนที่จะถูกนำไปใช้ ใช้ชั้นของการตรวจสอบเหล่านี้ตามสิ่งที่ระยะไกลทำ

การตรวจสอบบริบท/การอนุญาต

สิ่งนี้จำเป็นสำหรับการเข้าถึงระยะไกลที่มีผลกระทบต่อสถานะ ความก้าวหน้า หรือผู้เล่นคนอื่น เซิร์ฟเวอร์ต้องตรวจสอบว่า ผู้เล่นมีสิทธิ์ที่จำเป็นในการทำคำขอหรือไม่ ตัวอย่างเช่น ผู้เล่นอยู่ใกล้กับร้านพอที่จะซื้ออะไรบางอย่างไหม? พวกเขามีกุญแจที่จำเป็นในการเปิดประตูไหม? ตัวละครของพวกเขายังมีชีวิตอยู่ไหม?

การตรวจสอบเป็นสิ่งจำเป็นสำหรับระยะไกลที่:

  • มอบรางวัลหรือปรับเปลี่ยนความก้าวหน้าของผู้เล่น
  • มีผลกระทบต่อผู้เล่นคนอื่นหรือสถานะประสบการณ์ที่แชร์กัน
  • ปฏิบัติกับการกระทำที่มีข้อกำหนดในด้านระยะทาง เวลา หรือการอนุญาต (ตัวอย่างเช่น ระยะทางของร้าน)

การตรวจสอบประเภทและโครงสร้าง

การโจมตีอีกอย่างที่ผู้แสวงหาผลประโยชน์สามารถทำได้คือการส่งประเภทที่ถูกต้องตามเทคนิค แต่ทำให้มีขนาดใหญ่ ยาว หรือผิดรูปมาก ตัวอย่างเช่น หากเซิร์ฟเวอร์ต้องดำเนินการที่มีค่าใช้จ่ายสูงบนสตริงที่มีขนาดตรงกับความยาว ผู้แสวงหาผลประโยชน์อาจส่งสตริงที่มีขนาดใหญ่มากหรือผิดรูปเพื่อลดประสิทธิภาพของเซิร์ฟเวอร์

การโจมตีทั่วไปอีกอย่างที่ผู้แสวงหาผลประโยชน์อาจใช้คือการส่ง tables แทน Instance ข้อมูล payload ที่ซับซ้อนสามารถเลียนแบบสิ่งที่ควรจะเป็นการอ้างอิงวัตถุธรรมดา

ตัวอย่างเช่น หากมีระบบร้านค้าในประสบการณ์ in-experience shop ซึ่งข้อมูลรายการเช่นราคาเก็บไว้ในวัตถุ NumberValue ผู้แสวงหาผลประโยชน์อาจหลีกเลี่ยงการตรวจสอบทั้งหมดโดยการทำดังต่อไปนี้:

LocalScript ใน 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, -- ค่าลบอาจถูกใช้เช่นกัน ส่งผลให้ให้เงินแทนที่จะหักมัน!
},
}
-- ส่ง 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 ระหว่างลูกค้าและเซิร์ฟเวอร์เพื่อร้องขอการซื้อ อย่างไรก็ตาม เป็นการสำคัญที่เซิร์ฟเวอร์ซึ่งเป็นผู้จัดการที่เชื่อถือได้ที่สุดของประสบการณ์ ยืนยันว่าผู้ใช้มีเงินเพียงพอในการซื้อรายการ

ตัวอย่างการไหลของการซื้อจากลูกค้าไปยังเซิร์ฟเวอร์ผ่าน RemoteEvent
ตัวอย่างการไหลของการซื้อจากลูกค้าไปยังเซิร์ฟเวอร์ผ่าน 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 และเตรียมพร้อมที่จะจัดการกับกรณีที่ผลิตภัณฑ์ถูกมอบให้แล้วสำหรับใบเสร็จที่กำหนด

©2026 Roblox Corporation. Roblox, โลโก้ Roblox และ Powering Imagination เป็นส่วนหนึ่งของเครื่องหมายการค้าที่จดทะเบียน และไม่ได้จดทะเบียนของเราในสหรัฐฯ และประเทศอื่นๆ