How would I sandbox user-generated code in a script builder game?

  1. What do you want to achieve? Keep it simple and clear!
    I would like to create a sandboxed script environment that will prevent access to services such as DataStoreService, etc

  2. What is the issue? Include screenshots / videos if possible!
    I just don’t know where to start, I do know that it requires the usage of messing with script environment and metatables. Unfortunately I’m not really a expert at script environments and metatables. I also did search the DevForum regarding this topic but it seems like I can’t understand how they work correctly.

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    Nothing yet for now…

iirc you cant access datastoreservice on the client so u could just set their thing they type to the source of a localscript

1 Like

The script executor I made supports executing in both client-side and server-side code execution.

You dont need to make a script executor though you can just change a localscripts.Source property

The Source property of LuaScriptContainer (simply LocalScript/Script) requires higher permission level to be read or modified.

What type of sandbox you want to make again? Like how do you want to achieve this? Do you want to just sandbox a script that gets inserted automatically? If so is not possible. In my opinion there is no way to access a script environment unless you call getfenv from within the script or make a module which contains a function that when is called by a script, it changes its environment. But as Roblox said, getfenv de-optimizes the environment so I wouldn’t use it if I was you.

In the script executor for the script builder game I am making, it has:

  • A code editor text box
  • Execute button
  • Button to toggle to execute in the client or at the server

What I want to prevent is to sandbox code generated by players to not allow access to services such as DataStoreService, TeleportService, etc. AND to prevent other players from getting :Kick() or :Destroy() calls

You should try and loadstring the code the player is trying to run. This turns it into a function which in turn allows you to get and set the environment of the function.

To prevent the user from accessing certain services, you could then wrap a instance in which I’d recommend u to look at this tutorial. Wrapping with metatables, or "How to alter the functionality of Roblox objects without touching them"

Here are all objects that you should wrap so you wont have to dig for that yourself: game, Game, Workspace, workspace, Stats

You can’t access Script.Source on a game script, It can only be used in the command bar and plugins.

1 Like

Thank you for the link to the post, I will read it,
I use a custom loadstring module with FiOne as the interpreter

You should try and use the standard loadstring function.
The function gotten from that is way faster. Wrapping the objects sacrifice quite some speed already, and using a custom loadstring module might make it even worse.
After all, there isn’t really much of a point to use a custom loadstring module anyway.

Well then, I’ll consider using it then.

  1. How do I pass the sandboxed environment to the loadstring?

https://developer.roblox.com/en-us/api-reference/lua-docs/Lua-Globals

EDIT:
Awnsered myself, I remember setfenv() 's first argument can be the level or function

Thank you to @alicesays_hallo , @ayyildizlibayrak742 , @focasds , and @CoderHusk for trying to help me,
in the end I used the post that @alicesays_hallo linked and with his suggestion to use Roblox’s loadstring instead of using a custom lodstring module, and it works fine along with I now know how to sandbox code atleast, I appreciate your attempt to help me.

Final code I made:

local envWrapperCache = setmetatable({}, {__mode = "k"})

wrap = function(real)
	for w, r in next, envWrapperCache do
		if r == real then
			return w
		end	
	end

	if type(real) == "userdata" then
		local fake = newproxy(true)
		local meta = getmetatable(fake)
		meta.__index = function(s, k)
			if k == "Kick" or k == "kick" then
				return function(self)
					return nil --// https://developer.roblox.com/en-us/api-reference/function/Player/Kick :Kick() returns void
				end
			end
			if k == "Destroy" or k == "destroy" or k == "remove" or k == "Remove" and s == "Player" then
				return function(self)
					return nil --// https://developer.roblox.com/en-us/api-reference/function/Instance/Destroy returns void
				end
			end
			if k == "Teleport" or k == "TeleportAsync" or k == "TeleportPartyAsync" or k == "TeleportToPlaceInstance" or k == "TeleportToPrivateServer" or k == "TeleportToSpawnByName" or k == "SetTeleportSetting" or k == "SetTeleportGui" or k == "ReserveServer" or k == "GetTeleportSetting" or k == "GetPlayerPlaceInstanceAsync" or k == "GetLocalPlayerTeleportData" or k == "GetArrivingTeleportGui" and s == "TeleportService" then
				return function(self)
					return nil
				end
			end
			if k == "GetLogHistory" and s == "LogService" or s == "logservice" then
				return function(self)
					return {}
				end
			end
			if k == "MessageOut" and s == "LogService" or s == "logservice" then
				return Instance.new("BindableEvent").Event
			end
			if s == nil or s == "nil" then
				return nil
			end
			return wrap(real[k])
		end
		meta.__newindex = function(s, k, v)
			real[k] = v
		end
		meta.__tostring = function(s)
			return tostring(real)
		end
		meta.__metatable = getmetatable(real)
		envWrapperCache[fake] = real
		return fake
	elseif type(real) == "function" then
		local fake = function(...)
			local args = unwrap{...}
			local results = wrap{real(unpack(args))}
			return unpack(results)
		end
		envWrapperCache[fake] = real
		return fake
	elseif type(real) == "table" then
		local fake = {}
		for k, v in next, real do
			fake[k] = wrap(v)
		end
		return fake
	else
		return real
	end
end

unwrap = function(wrapped)
	if type(wrapped) == "table" then
		local real = {}
		for k, v in next, wrapped do
			real[k] = unwrap(v)
		end
		return real
	else
		local real = envWrapperCache[wrapped]
		if real == nil then --// wasn't wrapped or doesn't exist.
			return wrapped
		end
		return real
	end
end

local env = getfenv(0)
env.OwnerPlr = wrap(properties.plr)
env.game = wrap(env.game)
env.Game = wrap(env.Game)
env.workspace = wrap(env.workspace)
env.Workspace = wrap(env.Workspace)
env.typeof = wrap(env.typeof)
env.rawset = wrap(env.rawset)
env.rawget = wrap(env.rawget)
env.rawequal = wrap(env.rawequal)
--//env.type = wrap(env.type) can create a stack overflow error and crash Studio
env.print = wrap(env.print)
env.Print = wrap(env.Print)

It’s current flaw is that you cannot sandbox the “type” global, attempting to do so creates a stack-overflow error and crashes Roblox Studio

4 Likes