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.

warningLuaudit check: wall-only-access

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, CanCollide changed 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.