로블록스 RemoteEvent 보안 점검법: 서버 검증·거리·쿨다운 순서
RemoteEvent가 정상적으로 작동한다고 안전한 것은 아닙니다. 클라이언트는 “구매 버튼을 눌렀다”는 의도만 보내고, 가격·보유 재화·상점 거리·지급 결과는 서버가 다시 계산해야 합니다. 자료형 검사 하나로 끝내지 말고 값의 범위, 현재 상태, 권한과 호출 속도를 차례로 확인해야 조작된 요청과 실수로 반복된 요청을 함께 막을 수 있습니다.
2026년 10월 2일 Windows 데스크톱 브라우저에서 Roblox Creator Hub의 Securing the client-server boundary, Remote events and callbacks, Security and cheat mitigation tactics 공개 문서와 Markdown 원문을 대조했습니다. 클라이언트 입력 검증, 서버 권한, 자료형·구조·값 검사, NaN·무한대 차단, 호출 제한, RemoteEvent 전달 보장과 RemoteFunction의 대기 위험을 확인했습니다. 실제 Roblox Studio 프로젝트나 공격 도구를 실행하지 않았으므로 아래 테스트 결과는 특정 게임에서 직접 측정한 값이 아닙니다. 코드는 공식 원칙을 바탕으로 단순화한 점검용 예제이며 자신의 데이터 구조와 게임 규칙에 맞춰 테스트해야 합니다.
1. RemoteEvent가 보내졌다는 사실은 허가 증명이 아닙니다
공식 보안 문서를 읽으며 가장 먼저 고정한 기준은 클라이언트가 보낸 값은 요청일 뿐, 결과를 결정하는 사실이 아니라는 것입니다. 이용자의 LocalScript가 정상 버튼에서만 FireServer()를 호출하더라도, 조작된 클라이언트는 RemoteEvent와 RemoteFunction을 임의의 값과 빈도로 실행할 수 있습니다.
| 클라이언트가 알려도 되는 것 | 서버가 직접 확인할 것 | 서버가 결정할 결과 |
|---|---|---|
| 구매하려는 상품 ID | 실제 상품 존재, 서버 가격, 보유 재화, 상점 거리 | 차감·지급·실패 응답 |
| 발사 버튼과 조준 방향 | 장착 무기, 탄약, 발사 간격, 사거리·장애물 | 명중·피해·탄약 감소 |
| 문을 열려는 대상 ID | 열쇠 보유, 생존 상태, 거리, 문의 현재 상태 | 문 상태 변경 |
| 능력을 쓰려는 위치 | 능력 해금, 자원, 쿨다운, 유효 좌표·거리 | 효과 생성과 다른 클라이언트 통지 |
가격, 보상량, 피해량과 “검증 통과” 같은 결론을 클라이언트가 보내도록 설계하면 LocalScript의 버튼을 숨겨도 안전하지 않습니다. 서버는 자신이 보관한 설정과 상태로 모든 중요한 값을 다시 계산해야 합니다.
2. Remote 방향과 첫 번째 인자를 정확히 구분합니다
RemoteEvent는 응답을 기다리지 않는 비동기 단방향 통신입니다. 클라이언트는 FireServer(), 서버는 OnServerEvent를 사용하며 서버 콜백의 첫 번째 인자는 Roblox가 넣어 주는 Player입니다. 이 Player를 클라이언트 인수로 다시 받거나 사용자 이름 문자열로 대체하지 않습니다.
| 방향 | 보내는 쪽 | 받는 쪽 | 주요 용도 |
|---|---|---|---|
| Client → Server | FireServer(args) | OnServerEvent(player, args) | 행동 의도 요청 |
| Server → Client | FireClient(player, args) | OnClientEvent(args) | 특정 이용자 UI·효과 갱신 |
| Server → All | FireAllClients(args) | OnClientEvent(args) | 검증된 공용 상태·효과 전달 |
| Client → Server → Client | InvokeServer(args) | OnServerInvoke(player, args) | 즉시 반환값이 꼭 필요한 요청 |
RemoteEvent는 양쪽에서 볼 수 있는 ReplicatedStorage 등에 둘 수 있지만, 가격표·보상 계산·비밀 설정과 최종 처리 코드는 ServerScriptService 같은 서버 전용 위치에 둡니다. ReplicatedStorage의 ModuleScript는 실행하지 않더라도 클라이언트에 복제될 수 있으므로 비밀 로직 저장소가 아닙니다.
3. ‘클라이언트 값을 그대로 적용’하는 코드를 먼저 찾습니다
다음 구조는 버튼이 정상적으로 보일 때는 작동하지만 서버가 가격과 수량을 모두 클라이언트에게 맡깁니다. 조작된 요청이 음수 가격, 과도한 수량 또는 존재하지 않는 상품을 보낼 수 있습니다.
-- 위험한 서버 예시: 사용하지 마세요.
BuyItem.OnServerEvent:Connect(function(player, itemName, price, amount)
player.Coins.Value -= price
giveItem(player, itemName, amount)
end)
클라이언트 쪽에서 math.clamp()를 적용하거나 버튼에 쿨다운을 넣어도 서버 검증을 대신하지 못합니다. 정상 UI의 실수를 줄이는 장치는 될 수 있지만, 클라이언트 코드는 변경될 수 있고 Remote를 직접 호출할 수도 있습니다.
OnServerEvent 또는 OnServerInvoke 안에서 클라이언트가 보낸 가격·보상·피해량을 바로 더하거나 빼는 코드, 전달받은 Instance를 위치 확인 없이 삭제하는 코드, 전달받은 경로로 require()하거나 자산을 불러오는 코드를 우선 점검하세요.4. 자료형 → 값 → 상황·권한 → 빈도 순서로 검증합니다
공식 문서는 모든 클라이언트 입력을 서버가 검증하고, Remote의 목적에 맞는 여러 층을 적용하라고 안내합니다. 검사 순서를 정해 두면 비용이 싼 거절을 먼저 수행하고 중요한 상태 변경은 마지막에 할 수 있습니다.
- 자료형·구조:
typeof()로 문자열·숫자·Instance를 확인합니다. 문자열 길이와 table 항목 수를 제한하고, Instance라면 클래스와 예상 부모·경로를 함께 확인합니다. - 값: 숫자의 최소·최대뿐 아니라
math.isfinite()로 NaN과 무한대를 거절합니다. 문자열은 허용 목록에 있는 ID인지 확인하고 자유 입력은 길이·형식을 제한합니다. - 상황·권한: 캐릭터 존재와 생존, 서버가 가진 재화·인벤토리·퀘스트 상태, 대상과의 거리, 팀·소유권과 행동 가능 상태를 확인합니다.
- 빈도: 플레이어별 호출 간격이나 토큰 버킷을 서버에서 적용합니다. 클라이언트 쿨다운은 화면 연타 방지용 보조 장치로만 사용합니다.
Instance를 받는다면 typeof(value) == "Instance"만으로 부족합니다. 공격자가 접근 가능한 다른 Instance를 보낼 수 있으므로 IsDescendantOf(), IsA(), 태그 또는 서버의 허용 목록까지 확인하세요. 가능하면 Instance 전체보다 짧은 상품 ID처럼 검증하기 쉬운 값 하나만 받는 편이 단순합니다.
5. 클라이언트는 상품 ID만 보내고 서버가 구매를 완성합니다
아래 예제는 클라이언트가 itemId 하나만 보내고 서버가 상품표, 거리, 재화와 호출 간격을 확인하는 구조입니다. 실제 게임에서는 Coins와 인벤토리를 서버 데이터 모듈로 교체하고, 지급과 차감이 중간에 끊기지 않도록 하나의 서버 작업으로 처리하세요.
-- ServerScriptService의 Script 예시
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local BuyItem = ReplicatedStorage.Remotes.BuyItem
local shopPart = workspace.Shop.Counter
local SHOP = {
Potion = { price = 100, maxDistance = 12 },
Lantern = { price = 250, maxDistance = 12 },
}
local lastRequest = {}
local REQUEST_INTERVAL = 0.4
local function reject(player, reason)
warn(("BuyItem 거절: %s / %s"):format(player.UserId, reason))
end
BuyItem.OnServerEvent:Connect(function(player, itemId)
-- 1) 자료형과 길이
if typeof(itemId) ~= "string" or #itemId == 0 or #itemId > 32 then
reject(player, "invalid item id")
return
end
-- 2) 서버 허용 목록
local item = SHOP[itemId]
if not item then
reject(player, "unknown item")
return
end
-- 3) 플레이어별 서버 쿨다운
local now = time()
if now - (lastRequest[player.UserId] or 0) < REQUEST_INTERVAL then
reject(player, "too fast")
return
end
lastRequest[player.UserId] = now
-- 4) 캐릭터·생존·거리
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if not root or not humanoid or humanoid.Health <= 0 then
reject(player, "invalid character")
return
end
if (root.Position - shopPart.Position).Magnitude > item.maxDistance then
reject(player, "too far")
return
end
-- 5) 서버가 가진 재화 확인과 결과 적용
local coins = player:FindFirstChild("Coins")
if not coins or coins.Value < item.price then
reject(player, "not enough coins")
return
end
coins.Value -= item.price
grantItemOnServer(player, itemId)
end)
Players.PlayerRemoving:Connect(function(player)
lastRequest[player.UserId] = nil
end)
이 예제의 핵심은 코드 길이가 아니라 서버가 진실의 원본을 갖는 구조입니다. 클라이언트가 보내지 않은 가격을 서버의 SHOP에서 읽고, 서버가 관찰한 캐릭터 위치와 재화를 사용합니다. UI에는 서버가 복제한 결과만 표시하세요.
6. 쿨다운은 클라이언트와 서버에 각각 목적이 다릅니다
클라이언트 쿨다운은 정상 이용자의 중복 클릭을 줄이고 즉시 피드백을 주는 역할입니다. 서버 쿨다운은 조작된 호출과 과도한 요청에서 실제 로직, DataStore·BadgeService 같은 제한된 API와 다른 이용자를 보호합니다.
| 방식 | 적합한 상황 | 주의점 |
|---|---|---|
| 고정 간격 | 구매·문 열기처럼 드문 행동 | 짧은 정상 연속 행동까지 막지 않도록 실제 게임 속도에 맞춤 |
| 토큰 버킷 | 짧은 연속 입력은 허용하되 지속 스팸을 막아야 할 때 | 플레이어별 bucket 정리와 용량·회복 시간 기록 |
| 동시 실행 잠금 | 저장·거래처럼 같은 작업이 겹치면 안 될 때 | 오류가 나도 잠금이 풀리도록 정리 경로 필요 |
| 서버 전체 예산 | 외부 HTTP·DataStore 등 공유 한도가 있는 작업 | 개인별 제한만으로 서버 전체 폭주를 막지 못할 수 있음 |
거절된 호출을 모두 상세 로그로 남기면 로그 자체가 과부하가 될 수 있습니다. 플레이어별·사유별 횟수를 집계하고 일정 간격으로 요약하며, 정상 이용자도 네트워크 지연이나 버튼 중복 입력으로 제한에 걸릴 수 있으므로 한 번의 거절만으로 제재하지 않습니다.
7. RemoteFunction은 즉시 답이 필요할 때만 사용합니다
RemoteFunction은 호출한 쪽이 응답을 받을 때까지 대기합니다. 구매 성공 여부처럼 반환값이 꼭 필요할 수 있지만, 단순 UI 효과나 상태 알림에는 RemoteEvent와 서버 상태 복제가 더 안전한 경우가 많습니다.
- 클라이언트의
InvokeServer()인수도 RemoteEvent와 같은 검증을 모두 적용합니다. - 서버에서
InvokeClient()를 호출하면 클라이언트 오류가 서버에 전달되거나, 클라이언트가 나가면 오류가 나고, 응답하지 않으면 서버가 계속 대기할 수 있습니다. - 하나의 RemoteFunction에
OnServerInvoke를 여러 번 대입하면 마지막 콜백만 실행됩니다. - RemoteEvent와 RemoteFunction 사이의 상대적 순서는 보장되지 않으므로 순서가 중요하면 같은 Remote 종류를 사용하거나 sequence 번호를 검증합니다.
RemoteEvent는 신뢰성 있게 순서대로 전달되지만 처리 함수가 양보하면 다음 핸들러가 먼저 시작될 수 있습니다. “이벤트가 순서대로 도착한다”와 “앞 작업이 완전히 끝난 뒤 다음 작업이 실행된다”는 같은 의미가 아닙니다. 중복 실행이 위험한 거래·저장에는 서버 잠금이나 상태 전이를 추가하세요.
8. 정상 버튼만 누르지 말고 실패 입력을 직접 준비합니다
공격 도구를 사용할 필요는 없습니다. Studio의 서버 테스트에서 Remote 처리 함수를 별도 ModuleScript로 분리하고 아래 입력을 단위 테스트하거나, 개발 전용 테스트 코드에서 같은 검증 함수를 호출해 거절 이유를 확인할 수 있습니다.
| 시험 입력 | 예상 서버 결과 | 놓치기 쉬운 문제 |
|---|---|---|
nil, number, table을 itemId로 전달 | 상태 변경 없이 자료형 단계에서 거절 | 문자열 변환으로 잘못 수용 |
| 33자 이상 문자열·없는 ID | 길이 또는 허용 목록에서 거절 | 동적 경로·이름 검색으로 비싼 작업 수행 |
| 상점에서 멀리 떨어진 정상 ID | 거리 검사에서 거절 | 클라이언트가 보낸 위치를 사용 |
| 재화 부족·사망 상태 | 차감·지급 없이 거절 | 먼저 지급한 뒤 실패 처리 |
| 0.4초 안에 연속 호출 | 첫 허용 뒤 나머지 제한 | 모든 플레이어가 공유하는 하나의 쿨다운 |
| NaN·무한대 숫자 | math.isfinite()에서 거절 | typeof가 number라 범위 검사를 우회 |
각 시험에서 재화, 인벤토리, DataStore 호출 횟수와 다른 플레이어의 화면이 바뀌지 않았는지 함께 봅니다. 서버 로그에는 사용자 ID, Remote 이름, 거절 사유와 시각만 남기고 클라이언트가 보낸 전체 자유 입력이나 개인정보는 그대로 기록하지 마세요.
9. 배포 전에는 Remote마다 한 줄 위협 모델을 작성합니다
- 이 Remote가 최악의 값으로 초당 1,000번 호출되면 무엇이 바뀌는지 적습니다.
- 클라이언트가 보내는 인수를 최소화하고 서버가 다시 계산할 값을 지웁니다.
- 자료형·크기·유한 숫자·허용 목록 검사를 가장 앞에 둡니다.
- 서버의 플레이어 상태, 권한, 거리와 쿨다운을 확인합니다.
- 검증이 모두 끝난 뒤에만 재화·인벤토리·공용 상태를 변경합니다.
- Remote가 다른 클라이언트로 전달될 때 서버가 단순 중계기가 되지 않는지 확인합니다.
- Studio의 Server & Clients에서 정상·실패 입력을 반복하고 공개 서버의 F9 Server Log를 확인합니다.
공개 서버에서 Client·Server 로그와 Network를 분리하는 방법은 F9 Developer Console 확인법을 참고하세요. Remote에서 DataStore 저장까지 이어진다면 DataStore 저장 실패 점검법과 DataStore 요청 예산 계산기로 호출량도 따로 확인합니다.
공식 출처·갱신일·적용 범위
아래 Roblox Creator Hub 1차 자료를 2026년 10월 2일 확인했습니다. 공개 Markdown 메타데이터에는 세 문서 모두 2026년 10월 1일 21:21:10 UTC 갱신으로 표시됐습니다.
- Securing the client-server boundary — 상황·권한, 자료형·구조, 값과 NaN·무한대 검사, 서버 측 호출 제한, 상점·무기·클라이언트 중계 사례를 확인했습니다.
- Remote events and callbacks — RemoteEvent·RemoteFunction 방향, Player 인수, 전달 순서·대기·큐, 복제되지 않는 Instance와 metatable 제한을 확인했습니다.
- Security and cheat mitigation tactics — 클라이언트를 신뢰하지 않는 원칙, 서버 권한, 위협 모델과 서버 전용 로직 배치 기준을 확인했습니다.
적용 범위: Roblox Studio에서 클라이언트가 RemoteEvent 또는 RemoteFunction으로 서버의 구매·보상·상호작용·능력 로직을 요청하는 일반 구조를 점검합니다. 특정 게임의 안티치트 완성, 네트워크 소유권 기반 물리 검증, 암호화, 제재 자동화와 실제 공격 탐지는 다루지 않습니다. 예제 수치와 상점 구조는 설명용이므로 자신의 정상 입력 빈도·지연·게임 규칙을 측정해 조정하고 최신 Creator Hub 문서를 우선하세요.