The risks of loading models by id while the game runs
A model loaded at runtime brings whatever scripts its latest version holds. How to keep maps in the place file or load them sandboxed.
What it looks like
The server loads a model by asset id while the game runs, with InsertService:LoadAsset or AssetService:LoadAssetAsync, and puts it in the game. The id is often hardcoded, or read from a data store, a web request or a settings value so the map or event can be changed without updating the game. Whatever scripts the published model holds at that moment come with it.
local map = InsertService:LoadAsset(eventMapId)
map.Parent = workspaceHow to detect it
- Find every
InsertService:LoadAsset,InsertService:LoadAssetVersionandAssetService:LoadAssetAsynccall, and see where the result is parented. - Look at where the id comes from. A hardcoded id loads whatever that asset holds now, not what was tested. An id from
HttpService, a data store, a memory store orMessagingServicelets anyone who controls that source choose what loads. - Check whether the loaded model may contain scripts, and whether they can run.
AssetService:LoadAssetAsyncreturns the model sandboxed with no script capabilities by default. Look for code that turnsSandboxedoff or grants itCapabilitiessuch as Network, DataStore, AssetRequire or LoadString, which Roblox's third-party assets guide says backdoors commonly use. Treat scripts in a model fromInsertService:LoadAssetas able to run once it's parented somewhere scripts run. AllowInsertFreeAssets, set in Studio's **Allow Loading Third Party Assets**, letsLoadAssetAsyncalso load any public free asset on the Creator Store, not just assets the creator owns or that were shared with them. If the code loads ids it doesn't control, check whether that setting is on.- If the client chooses the id, report Assets loaded by an id the client chose instead.
What an exploiter could do
Anyone who can publish a new version of the model, or change the data store entry, web response or message that names it, puts their own scripts into every server that loads it. That is the same reach as Module loaded by asset id: admin for themselves, hidden remotes, or content that gets the game moderated. A compromised account on the team is enough.
How to fix it
- Keep maps and models that ship with scripts in the place file, in
ServerStorage, and clone them from there. - If a model must be loaded at runtime, load it with
AssetService:LoadAssetAsync, keep it sandboxed, and grant only the capabilities it needs. - Only load ids from a fixed list in the server code, and strip scripts from loaded models you only need for their parts.
Vulnerable example
local DataStoreService = game:GetService("DataStoreService") local InsertService = game:GetService("InsertService") local Workspace = game:GetService("Workspace") local config = DataStoreService:GetDataStore("LiveConfig") local ok, mapId = pcall(config.GetAsync, config, "EventMap") if ok and typeof(mapId) == "number" then local loaded, map = pcall(InsertService.LoadAsset, InsertService, mapId) if loaded and map then map.Parent = Workspace end end
Fixed example
local DataStoreService = game:GetService("DataStoreService") local ServerStorage = game:GetService("ServerStorage") local Workspace = game:GetService("Workspace") local config = DataStoreService:GetDataStore("LiveConfig") local maps = ServerStorage:WaitForChild("EventMaps") local ok, mapName = pcall(config.GetAsync, config, "EventMap") local map = ok and typeof(mapName) == "string" and maps:FindFirstChild(mapName) if map then map:Clone().Parent = Workspace end
Common false positives
- Loading avatar items, decals, sounds or meshes, which can't hold scripts, isn't this check.
- A model loaded with
LoadAssetAsyncand left sandboxed with no capabilities can't run its scripts. - Loading in Studio only, behind
RunService:IsStudio(), for build tools doesn't reach live servers.