Why teleport data can't be trusted in Roblox
TeleportData passes through the client, even with a SourcePlaceId check. How to carry coins, boosts and entry fees between places safely.
What it looks like
One place teleports players to another and passes data along with TeleportOptions:SetTeleportData(): coins, a luck boost, an entry fee, a rank. The destination reads it from Player:GetJoinData().TeleportData and uses it to pay out, unlock things or decide what the player is allowed to do.
Luau
local data = player:GetJoinData().TeleportData
profile.Coins += data.coinsHow to detect it
- Search for
GetJoinData,TeleportData,LaunchData,GetLocalPlayerTeleportDataandSetTeleportData. - Treat the value as client-controlled if it changes currency, items, levels, multipliers, rewards, roles or permissions, or if a remote reads it later.
- A check on
SourcePlaceIdorSourceGameIddoesn't make the data safe. It proves which place sent the teleport, not that the data arrived untouched. LaunchDatacomes from share links and invites, so anyone can write it.GetLocalPlayerTeleportDataruns on the client and has none of the guaranteesGetJoinDatagives.
What an exploiter could do
Roblox's documentation says teleport data is visible to the client and unencrypted, and that because it's transmitted by the client it can still be abused by an exploiter. If the destination pays out or unlocks things from it, they can give themselves any amount, any multiplier or any rank by changing the data in transit.
How to fix it
- Keep anything of value in a data store or memory store keyed by the player's
UserId, and have the destination read it from there. This is what Roblox recommends for currency and inventory. - Teleport data is fine for hints that give nothing away, such as which spawn or map a party picked. Check its type and match it against an allowlist, the same as a remote argument.
- When the source place needs to hand something over, such as an entry fee already taken, write it to a memory store before the teleport and have the destination read and remove it.
- If only some places should be able to send players in, also check
SourcePlaceId, but treat that as a filter, not as proof the data is genuine.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local luck: { [Player]: number } = {} Players.PlayerAdded:Connect(function(player: Player) local data = player:GetJoinData().TeleportData local boost = if typeof(data) == "table" then rawget(data, "luck") else nil if typeof(boost) == "number" and boost >= 0 and boost <= 10 then luck[player] = boost end end) Players.PlayerRemoving:Connect(function(player) luck[player] = nil end) local function luckFor(player: Player): number return luck[player] or 0 end return luckFor
Fixed example
Fixed
local PlayerData = require(script.Parent.PlayerData) local function luckFor(player: Player): number local profile = PlayerData.get(player) if not profile then return 0 end return profile.Boosts.Luck end return luckFor
Common false positives
- Teleport data used only for cosmetic or convenience choices, checked against an allowlist, gives an exploiter nothing.
- Data read from a data store or memory store after the teleport isn't teleport data, even if the teleport triggered the read.
ReferredByPlayerIdandMembersaren't teleport data. Rewarding referrals has its own problems, but they belong to other checks.