Why a VIP door isn't enough to protect a VIP room
Exploiters can walk through walls and doors. How to check access on the server for everything inside a restricted area.
What it looks like
A VIP room, staff area, paid zone or locked level is kept closed by a wall, a door or a collision group that only lets some players through. Inside are rewards, shops or tools that assume anyone standing there has access, so the server never checks again.
Luau
chest.ProximityPrompt.Triggered:Connect(function(player) giveCoins(player, 500) end)
How to detect it
- Find areas gated by geometry: doors that open per player,
CanCollidechanged from a LocalScript, collision groups that only some characters pass, and invisible barriers. - Roblox's network ownership guide says a client that owns its character can bypass physical obstacles, alter its collisions and teleport anywhere. Walls stop honest players only.
- For everything inside the area that grants or sells something, check the handler confirms the player's access on the server: the game pass, rank, level or key that the door was checking.
- Roblox's security guide gives "Do they have a key required to open a door?" as an example of the permission check a server needs for requests that affect the game.
- Doors that open from a remote are a separate question. Judge those with the usual remote checks.
What an exploiter could do
They walk through the wall or teleport inside, then use everything there: VIP chests, staff tools, paid-zone shops and late-game rewards, without the pass, rank or progress the area was meant to require.
How to fix it
- Check the player's access on the server in every handler inside the area, the same way the door does.
- Keep the door as well, for honest players, but treat it as decoration.
- Optionally, check on the server whether a player without access is inside the area, and move them out.
Vulnerable example
Vulnerable
local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local vipPass = 1234567 local lounge = Workspace:WaitForChild("VipLounge") local door = lounge:WaitForChild("Door") :: BasePart local chest = lounge:WaitForChild("Chest"):WaitForChild("ProximityPrompt") :: ProximityPrompt local claimed: { [Player]: boolean } = {} Workspace:RegisterCollisionGroup("VipDoor") Workspace:RegisterCollisionGroup("VipPlayers") Workspace:CollisionGroupSetCollidable("VipDoor", "VipPlayers", false) door.CollisionGroup = "VipDoor" Players.PlayerAdded:Connect(function(player) local ok, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, vipPass) if not ok or not owns then return end player.CharacterAdded:Connect(function(character) for _, part in character:GetDescendants() do if part:IsA("BasePart") then part.CollisionGroup = "VipPlayers" end end end) end) chest.Triggered:Connect(function(player) local profile = PlayerData.get(player) if profile and not claimed[player] then claimed[player] = true profile.Coins += 500 end end) Players.PlayerRemoving:Connect(function(player) claimed[player] = nil end)
Fixed example
Fixed
local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local vipPass = 1234567 local lounge = Workspace:WaitForChild("VipLounge") local door = lounge:WaitForChild("Door") :: BasePart local chest = lounge:WaitForChild("Chest"):WaitForChild("ProximityPrompt") :: ProximityPrompt local vip: { [Player]: boolean } = {} local claimed: { [Player]: boolean } = {} Workspace:RegisterCollisionGroup("VipDoor") Workspace:RegisterCollisionGroup("VipPlayers") Workspace:CollisionGroupSetCollidable("VipDoor", "VipPlayers", false) door.CollisionGroup = "VipDoor" Players.PlayerAdded:Connect(function(player) local ok, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, vipPass) if not ok or not owns then return end vip[player] = true player.CharacterAdded:Connect(function(character) for _, part in character:GetDescendants() do if part:IsA("BasePart") then part.CollisionGroup = "VipPlayers" end end end) end) chest.Triggered:Connect(function(player) local profile = PlayerData.get(player) if vip[player] and profile and not claimed[player] then claimed[player] = true profile.Coins += 500 end end) Players.PlayerRemoving:Connect(function(player) vip[player] = nil claimed[player] = nil end)
Common false positives
- Areas where nothing is granted, bought or unlocked, such as a decorative VIP room, only matter for looks.
- Handlers inside the area that already check the same access on the server, like the fixed example, are fine.
- Separate places for paid or restricted content are Restricted places guarded only by a kick.