Why the server shouldn't check a game pass id the client sends
UserOwnsGamePassAsync answers for any pass on Roblox. How to keep your own table of pass ids and let the client pick only which perk to check.
What it looks like
The server checks game pass ownership properly, with UserOwnsGamePassAsync on the server, but the pass id it checks comes from the client, or the perk it grants is worked out from something about the pass the client picked rather than from the game's own list of passes.
remotes.ClaimPass.OnServerEvent:Connect(function(player, passId, perk) if MarketplaceService:UserOwnsGamePassAsync(player.UserId, passId) then grantPerk(player, perk) end end)
How to detect it
- Find every
UserOwnsGamePassAsynccall and trace where its pass id comes from. It should be a constant or a value from a table kept in server code. - A pass id taken from a remote argument, a
RemoteFunctionanswer, or an attribute or value the client can change is a finding, even when the check itself runs on the server. - Check how the perk is chosen. It must come from the game's own mapping of pass id to perk. A perk named by the client, or found from the pass's name, description or price through
GetProductInfoAsync, is a finding. - A server table that maps the client's choice to a pass id, such as
passes[perkName], is the safe shape: the client only picks which pass to check. - Do the same for
PromptGamePassPurchaseFinishedhandlers. Granting a perk for whatevergamePassIdthe event reports, without matching it against the game's own passes, has the same problem.
What an exploiter could do
Roblox's reference describes UserOwnsGamePassAsync as checking whatever pass id it's given, with nothing limiting it to this game's passes. An exploiter makes a cheap pass in their own experience, or uses any pass they already own, and sends its id. If the server trusts the id, or reads the perk from the pass's name, they get every perk the game sells, such as VIP, double coins or exclusive items, without buying any of them.
How to fix it
- Keep a server table from each of the game's pass ids to the perk it unlocks.
- Let the client send at most a perk name, look up the pass id in that table, and check ownership of that id.
- Never decide what to grant from anything about a pass the client chose, including its name or price.
Vulnerable example
local MarketplaceService = game:GetService("MarketplaceService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local perks: { [string]: (Player) -> () } = { DoubleCoins = function(player) player:SetAttribute("CoinMultiplier", 2) end, Vip = function(player) player:SetAttribute("Vip", true) end, } ReplicatedStorage.Remotes.ClaimPass.OnServerEvent:Connect(function(player: Player, passId: unknown, perk: unknown) if typeof(passId) ~= "number" or typeof(perk) ~= "string" or not perks[perk] then return end local ok, owned = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, passId) if ok and owned then perks[perk](player) end end)
Fixed example
local MarketplaceService = game:GetService("MarketplaceService") local ReplicatedStorage = game:GetService("ReplicatedStorage") type Perk = { passId: number, grant: (Player) -> (), } local perks: { [string]: Perk } = { DoubleCoins = { passId = 9100001, grant = function(player) player:SetAttribute("CoinMultiplier", 2) end, }, Vip = { passId = 9100002, grant = function(player) player:SetAttribute("Vip", true) end, }, } ReplicatedStorage.Remotes.ClaimPass.OnServerEvent:Connect(function(player: Player, name: unknown) local perk = if typeof(name) == "string" then perks[name] else nil if not perk then return end local ok, owned = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, perk.passId) if ok and owned then perk.grant(player) end end)
Common false positives
- A client that sends a pass id only to open a purchase prompt is fine. Prompting isn't granting, and the purchase still goes through Roblox.
- Showing which passes a player owns, for a shop menu, from a client-chosen id gives nothing away if nothing is granted.
- Checking passes from the game's own list against a client-chosen perk name, as above, is the fix, not a finding.