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.

criticalLuaudit check: trusted-teleport-data

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.coins

How to detect it

  • Search for GetJoinData, TeleportData, LaunchData, GetLocalPlayerTeleportData and SetTeleportData.
  • 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 SourcePlaceId or SourceGameId doesn't make the data safe. It proves which place sent the teleport, not that the data arrived untouched.
  • LaunchData comes from share links and invites, so anyone can write it. GetLocalPlayerTeleportData runs on the client and has none of the guarantees GetJoinData gives.

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.
  • ReferredByPlayerId and Members aren't teleport data. Rewarding referrals has its own problems, but they belong to other checks.