Why the server should never load an asset id the client sends
LoadAsset with a client's id can pull any model the creator owns into the live game. How to use server-side lists and AssetTypeVerification.
What it looks like
The client sends an asset id, and the server loads it: a model for a showroom, a decoration, a pet or an avatar item. The server passes the id to InsertService:LoadAsset, AssetService:LoadAssetAsync, or a HumanoidDescription it applies to a character.
Luau
remotes.Preview.OnServerEvent:Connect(function(player, assetId) local model = InsertService:LoadAsset(assetId) model.Parent = workspace end)
How to detect it
- Find every call to
InsertService:LoadAsset,InsertService:LoadAssetVersion,AssetService:LoadAssetAsync,Humanoid:ApplyDescriptionAsync,Humanoid:ApplyDescriptionResetAsync,Player:LoadCharacterWithHumanoidDescriptionAsyncandPlayers:CreateHumanoidModelFromDescriptionAsync, plus the older versions of those four withoutAsync. Trace each asset id back to its source. - An id from a remote argument, or a
HumanoidDescriptionbuilt from ids the client sent, is the finding unless the server matches every id against its own list first. LoadAssetcan load anything the game's creator owns or has been shared, as well as Roblox-owned assets and clothing and avatar accessories, which Roblox marks as open to any game. That includes models the creator never meant to ship: test builds, admin tools, unreleased maps. A client that picks the id picks from all of them.- For avatar loading, check the
AssetTypeVerificationargument. Roblox's reference for it says loading models can be insecure if malicious users can trick the game into loading unexpected models the creator owns, which may include malicious scripts. It recommendsEnum.AssetTypeVerification.Alwaysunless the game needs non-catalog assets. Missing orDefaultwith client ids is a finding. AssetService:LoadAssetAsyncreturns the model sandboxed with no script capabilities by default. Treat scripts in a model fromInsertService:LoadAssetas able to run once it's parented somewhere scripts run.- Check what happens to the loaded model: parented to
Workspaceor a character, cloned for everyone, or kept on the server.
What an exploiter could do
They send the id of any model the developer owns and it appears in the live server, with whatever scripts, tools or unreleased content it holds. Scripts in an admin or test model can give them power over the server. Avatar items they choose skip the game's own shop and unlocks.
How to fix it
- Let the client send a key from the game's own list, such as
"Sofa", and look up the asset id on the server. - Use
AssetService:LoadAssetAsyncand leave the loaded model sandboxed unless it's one you trust. - Pass
Enum.AssetTypeVerification.Alwayswhen applying descriptions, and build the description on the server from checked ids.
Vulnerable example
Vulnerable
local InsertService = game:GetService("InsertService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Workspace = game:GetService("Workspace") local stage = Workspace:WaitForChild("Showroom") ReplicatedStorage.Remotes.ShowModel.OnServerEvent:Connect(function(player: Player, assetId: unknown) if typeof(assetId) ~= "number" then return end local ok, model = pcall(InsertService.LoadAsset, InsertService, assetId) if ok and model then stage:ClearAllChildren() model.Parent = stage end end)
Fixed example
Fixed
local AssetService = game:GetService("AssetService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Workspace = game:GetService("Workspace") local stage = Workspace:WaitForChild("Showroom") local models: { [string]: number } = { Sofa = 9100101, Lamp = 9100102, } ReplicatedStorage.Remotes.ShowModel.OnServerEvent:Connect(function(player: Player, name: unknown) local assetId = typeof(name) == "string" and models[name] if not assetId then return end local ok, model = pcall(AssetService.LoadAssetAsync, AssetService, assetId) if ok and model then stage:ClearAllChildren() model.Parent = stage end end)
Common false positives
- Ids the server picks from its own table, even when the client chooses which entry, like the fixed example, are fine.
GetHumanoidDescriptionFromUserIdAsyncfor a real player's own avatar loads only what that player wears.- Anyone can load any number of things, so a showroom still needs a rate limit. Report that as Remote with no rate limit, not here.