클라이언트가 서버에서 작업을 트리거할 수 있는 경우, 남용의 가능성이 있습니다. RemoteEvents와 RemoteFunctions가 여기서 가장 일반적인 공격 벡터이지만, ProximityPrompt와 같은 다른 인스턴스도 취약합니다. 이 섹션에서는 이 경계를 보호하는 방법을 다룹니다.
원격 입력 검증
클라이언트에서 전송되는 모든 데이터 조각은 사용되기 전에 서버에서 검증해야 합니다. 원격이 수행하는 작업에 따라 이러한 검증 레이어를 적용하세요.
컨텍스트/권한 검증
이는 경험의 상태, 진행 또는 다른 플레이어에 영향을 미치는 원격에 필요합니다. 서버는 플레이어가 요청을 수행하는 데 필요한 권한을 가지고 있는지 확인해야 합니다. 예를 들어, 플레이어가 물건을 사기 위해 상점에 충분히 가까운가요? 문을 열기 위해 필요한 키가 있는가요? 캐릭터가 살아 있는가요?
검증이 필요한 원격은 다음과 같습니다:
- 보상을 부여하거나 플레이어 진행을 수정하는 원격
- 다른 플레이어 또는 공유 경험 상태에 영향을 미치는 원격
- 거리, 타이밍 또는 권한 요구 사항이 있는 행동을 수행하는 원격 (예: 상점 거리)
유형 및 구조 검증
착취자가 시도할 수 있는 또 다른 공격은 기술적으로 유효한 유형을 보내지만 매우 크거나 길거나 그렇지 않으면 잘못된 형태로 만드는 것입니다. 예를 들어, 서버가 길이에 따라 확장되는 문자열에서 비싼 작업을 수행해야 하는 경우, 착취자는 서버를 방해하기 위해 믿을 수 없을 만큼 크거나 잘못된 문자열을 보낼 수 있습니다.
착취자가 사용할 수 있는 또 다른 일반적인 공격은 tables를 Instance 대신 보내는 것입니다. 복잡한 페이로드는 일반 객체 참조처럼 보일 수 있습니다.
예를 들어, 아이템 데이터와 가격과 같은 항목 데이터가 NumberValue 객체에 저장되는 경험 내 상점 시스템을 제공받는 경우, 착취자는 다음과 같이 모든 다른 검사를 우회할 수 있습니다:
LocalScript in 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, -- 음수 값도 사용할 수 있으며, 결과적으로 통화를 주는 결과를 초래할 수 있습니다!},}-- 서버에 악성 페이로드 전송 (거부됩니다)print(buyItemEvent:InvokeServer(payload)) -- "false Invalid item provided" 출력-- 서버에 실제 아이템 전송 (통과합니다!)print(buyItemEvent:InvokeServer(itemDatafolder["Real Blade"])) -- "true" 출력 및 구매가 성공할 경우 남은 통화
Script in 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를 통해 전달된 값이 유효하고 요청된 컨텍스트에서 논리적인지 확인해야 합니다. 이는 통화, 아이템, 숫자 입력 또는 사용자 생성 콘텐츠를 처리하는 원격에 필수적입니다. 값이 예상 경계 내에 있는지 확인하고 (예: 구매 수량이 0보다 큰지), 제공된 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은 "숫자" 유형이지만 모든 표준 비교를 실패하게 만들기 때문에 독특하게 위험합니다. 이는 안전해 보이는 논리 검사를 미묘하게 우회할 수 있게 합니다. 플레이어가 제안을 생성하는 취약한 거래 시스템을 살펴보겠습니다.
-- 취약한 서버 코드
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 등), 착취자나 심지어 정당한 사용자에 의해 이 로직이 스팸이 될 가능성이 있습니다. 속도 제한은 이러한 행동이 얼마나 자주 수행될 수 있는지를 제어하여 남용과 시스템 과부하를 방지합니다. 클라이언트 측 속도 제한을 구현하는 것이 실용적이거나 때때로 필요하지만, 클라이언트 측 속도 제한에만 의존하지 마십시오.
클라이언트에 의해 트리거될 수 있는 모든 서버 측 로직에 대해 이러한 로직이 실행될 수 있는 최대 속도를 항상 고려하십시오. 많은 서버 측 작업은 남용이나 실패를 방지하기 위해 빈도를 제한해야 합니다. 예를 들어:
- 비용이 많이 드는 작업: 많은 서버 리소스를 소모하거나 다른 클라이언트에 영향을 미치는 작업
- 서버 예시: 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개의 요청 폭발을 허용하고 2초마다 1개의 토큰을 리필하는 제한기를 생성합니다(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
-- 10초 동안 5개의 메시지 (용량 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와 같은 속성을 무시합니다. 이 객체는 원격만큼 rigorously 보호해야 합니다.
ProximityPrompt
- 착취자는 서버에서 Enabled가 false인 경우에도 모든 이벤트를 발사할 수 있습니다.
- 클라이언트에서 Enabled, MaxActivationDistance, 및 RequiresLineOfSight를 조용히 변경할 수 있으므로, 복제된 ProximityPrompt는 어떤 위치에서도 보이고 잠재적으로 상호작용될 수 있습니다.
- 서버는 서버 측의 HoldDuration가 0인 경우에도 유지 이벤트(예: PromptButtonHoldBegan)를 수용합니다. 클라이언트는 HoldDuration를 조작하여 불과 몇 번의 상호작용을 비정상적으로 빠르게 수행할 수 있습니다.
- 오직 Triggered 이벤트만 서버 측 거리 확인을 가지고 있습니다. 다른 이벤트(예: PromptButtonHoldBegan 및 TriggerEnded)는 거리 확인이 없으며, 어떤 클라이언트든지 임의로 전송할 수 있습니다.
ClickDetector
- 서버에서 어떤 이벤트에 대해서도 전혀 확인이 없습니다.
- 착취자는 ClickDetector가 비활성화된 경우에도(0의 MaxActivationDistance 또는 DragDetector.Enabled가 false인 경우)에 모든 이벤트를 복제할 수 있습니다.
- ClickDetector가 클릭 가능한 것에 부모가 되지 않더라도 이벤트를 복제할 수 있습니다.
DragDetector
- ClickDetector에서 상속된 사건에 대해서는 어떤 체크도 없습니다.
- 드래그 이벤트에 대해서는 Enabled 속성과 PermissionPolicy 속성이 서버에서 확인되고 존중됩니다. 모든 다른 속성은 확인되지 않습니다.
이러한 이벤트가 발생할 때, 서버 스크립트는 조치를 취하기 전에 자체 검증을 수행해야 합니다:
- 사용 가능한지 확인: 인스턴스가 사용 가능하도록 의도된 것인지 속성(예: Enabled, HoldDuration, RunLocally 등)을 확인하여 서버에서 확인하십시오. 이 단계는 객체가 항상 활성화된 경우 필요하지 않습니다.
- 플레이어 상태 확인: 플레이어가 현재 상태로 인해 동작을 수행할 수 있는지 확인하십시오. 플레이어의 캐릭터가 존재하는가? 캐릭터가 객체에 충분히 가까운가? 플레이어가 이 객체와 상호작용하기 위해 살아 있어야 하는가? 필요한 경우 서버에서 사용자의 경험에 의해 유지되는 다른 플레이어 상태를 확인하세요.
- 속도 제한 적용: 속도 제한에서 언급된대로 클라이언트 측 쿨다운을 적용하고 서버에서 속도 제한을 시행하여 이러한 이벤트가 스팸되거나 남용되지 않도록 방지합니다. 최소한의 시간을 요구하는 상호작용의 경우, 클라이언트가 너무 빠르게 완료하지 않도록 확인하세요.
네트워크 소유권에 대한 참고 사항: 기본적으로 이러한 객체 중 어느 것도 고정되지 않은 부품 또는 고정되지 않은 조립체의 자식인 경우, 착취자는 부모 부품의 네트워크 소유권을 가져가서 직접 캐릭터로 이동시킬 수 있습니다. 이는 거리 확인을 우회합니다. 중요한 작업의 경우, 고정된 부품을 사용하거나 네트워크 소유권 API와 필요한 서버 측 검증을 함께 사용하십시오.
RemoteEvents 및 RemoteFunctions
RemoteFunctions 및 RemoteEvents의 범위와 영향을 제한하십시오. 원격 함수/이벤트를 기반으로 동적으로 자산(텍스처나 소리 포함)을 로드하거나 경험 코드를 실행하는 경우에는 극도로 주의하십시오. 서버가 삭제하거나 수정하도록 지정할 수 있는 임의의 경로 또는 인스턴스 참조를 클라이언트에서 지정하는 것을 허용하는 원격을 구현하는 것은 피하십시오. 임의의 인스턴스 상태를 변경하거나 전체 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("번개가 타격하는 위치: " .. 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이 NaN을 탐지하는 유일한 방법입니다)
if strikePosition.X ~= strikePosition.X then
return -- NaN이 포함되어 있습니다, 거부
end
-- 3. 예시 속도 제한
local lastCast = playerCooldowns[player] or 0
if tick() - lastCast < COOLDOWN_TIME then
return -- 여전히 쿨다운 중입니다
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)
이러한 검증을 구현하면 착취자가 다른 플레이어의 경험을 망치는 것이 더 어려워집니다. 서버는 보호막 역할을 하여 유효하고 권한이 있는 작업만 다른 클라이언트에 방송될 수 있도록 보장합니다.
특별 고려사항
다음은 특수한 처리가 필요할 수 있는 추가 사례입니다.
데이터 저장소 조작
플레이어 데이터를 저장하기 위해 DataStoreService를 사용하는 경험에서 착취자는 잘못된 데이터, 레이스 조건을 이용하여 저장을 손상시키거나 DataStore에서 아이템을 중복시킬 수 있습니다. 이는 아이템 거래, 시장 및 통화 시스템이 포함된 경험에서 특히 문제입니다.
클라이언트 입력으로 플레이어 데이터에 영향을 미치는 RemoteEvent 또는 RemoteFunction을 통해 수행되는 모든 작업이 다음을 기반으로 정화되도록 하세요:
- 인스턴스 값은 DataStore에 직렬화될 수 없으며 실패합니다. 이를 방지하기 위해 유형 검증을 활용하세요.
- DataStore에는 데이터 한계가 있습니다. 임의 길이의 문자열은 확인하고/또는 한도가 설정되어야 하며, 클라이언트가 임의의 키를 무한히 추가할 수 없도록 해야 합니다.
- 테이블 인덱스는 NaN 또는 nil이 될 수 없습니다. 클라이언트에 의해 전달되는 모든 테이블을 반복하며 모든 인덱스가 유효한지 확인하세요.
- DataStore는 유효한 UTF-8 문자만 수용할 수 있습니다, 따라서 utf8.len()을 통해 클라이언트에서 제공된 모든 문자열을 정화해야 합니다. utf8.len()은 문자열의 길이를 반환하며 유니코드 문자를 단일 문자로 취급합니다. 유효하지 않은 UTF-8 문자가 발견되면 nil과 잘못된 문자의 위치를 반환합니다. 잘못된 UTF-8 문자열은 키 및 값으로 테이블에 존재할 수도 있습니다.
- 무한대/NaN 숫자 착취 - 착취자는 -1/0, 0/0 또는 math.huge와 같은 값을 보내어 무한 통화를 유발하거나 계산을 파괴할 수 있습니다. 항상 NaN의 위험에서 설명된 방법을 사용하여 검증하세요.
레이스 조건은 플레이어가 기회를 조작하여 아이템을 중복시키거나 데이터를 손상시킬 때 발생할 수 있습니다.
거래 착취 예시: 플레이어가 거래를 시작하고, 아이템을 다른 플레이어에게 송신한 다음, 즉시 게임에서 나갑니다. 거래가 완료되지만 잘못된 데이터로 인해 DataStore 저장이 실패하면, 그들은 원래 아이템과 함께 다시 가입하며 다른 플레이어는 거래된 아이템을 유지하게 되므로 중복이 발생합니다.
예방 전략:
- 모든 데이터에 대해 거래 작업 전에 검증하세요.
- 모든 플레이어의 데이터가 검증된 후에 변경 사항을 커밋하는 트랜잭션 유사 패턴을 사용하세요.
- 일부가 실패할 경우 모든 변경 사항을 되돌릴 수 있도록 적절한 오류 처리를 구현하세요.
MarketplaceService
MarketplaceService와 같은 상호작용에 대해, 통과나 개발자 제품을 다룰 때, 모든 구매 검증 및 아이템 부여는 서버에서 수행되어야 합니다. 특히, ProcessReceipt 콜백을 활용하여 구매 영수증을 안전하게 검증해야 합니다. 클라이언트 측 신호(예: PromptProductPurchaseFinished)를 사용하여 서버 검증 없이 구매를 확인하는 것은 절대 신뢰해서는 안 됩니다. 클라이언트가 조작할 수 있기 때문입니다. ProcessReceipt 함수가 영수증 세부 사항을 철저히 확인하고 Roblox 서버와의 합법적인 거래가 확인된 후에만 아이템이나 통화를 부여해야 하며, 특정 영수증에 대해 이미 아이템이 부여된 경우를 처리할 준비를 해야 합니다.