Why loadstring is a backdoor risk in Roblox

Running code from strings with loadstring lets whoever controls the string run code on your server. How to remove it and use data instead.

criticalLuaudit check: dynamic-code

What it looks like

The server runs code it builds or receives as a string, with loadstring, usually after turning on ServerScriptService.LoadStringEnabled. The string comes from a remote, a web request, a data store or a value object.

Luau
loadstring(source)()

How to detect it

  • Search for loadstring in every server script and module, and check ServerScriptService.LoadStringEnabled in the project file or in Studio. loadstring only works on the server when that property is on.
  • For each call, trace where the string comes from. A remote argument, HttpService:GetAsync or RequestAsync, a data store, an attribute or a StringValue is a finding: whoever controls that source controls the server.
  • Look for wrappers that do the same thing, such as a ModuleScript that takes a string and runs it, or a third-party "interpreter" module.
  • LoadStringEnabled turned on with no loadstring in the code you can see is worth reporting as needs a closer look, since something may be relying on it.

What an exploiter could do

Anyone who can change the string can run any code on the server: give themselves currency or admin, delete the map, kick everyone or change saved data. A web address or a data store that leaks, or a remote that passes a string straight in, hands that power to an outsider.

How to fix it

  • Remove the loadstring call and turn LoadStringEnabled off.
  • Replace remote configuration with data: fetch settings as JSON with HttpService:JSONDecode, check each value's type and range, and use them as values, never as code.
  • Ship code as ModuleScripts in the game so it's reviewed and published with each update.

Vulnerable example

Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local developerId = 123456789

ReplicatedStorage.Remotes.DevConsole.OnServerEvent:Connect(function(player: Player, source: unknown)
	if player.UserId ~= developerId or typeof(source) ~= "string" then
		return
	end
	local run = loadstring(source)
	if run then
		run()
	end
end)

Fixed example

Fixed
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerData = require(script.Parent.PlayerData)

local developerId = 123456789

ReplicatedStorage.Remotes.DevSetCoins.OnServerEvent:Connect(function(player: Player, target: unknown, amount: unknown)
	if player.UserId ~= developerId then
		return
	end
	if typeof(target) ~= "Instance" or not target:IsA("Player") then
		return
	end
	if typeof(amount) ~= "number" or not (amount >= 0 and amount <= 1e6 and amount % 1 == 0) then
		return
	end
	local profile = PlayerData.get(target)
	if profile then
		profile.Coins = amount
	end
end)

Common false positives

  • A string that's only ever a constant written in the script itself, run once at start-up, is odd but not controlled by anyone outside. Recommend replacing it with normal code.
  • Admin modules that include a sandboxed script runner for staff are still this finding if they use loadstring. Say what the module is and that it should be checked.