Attempt to create a RBX.Lua sandbox

I just got the idea, personally I can’t find a use for it. I was hesitant to use loadstring, so I ended up usingmodulescripts instead.
Made this in about an hour, I wasn’t sure how to do it at first because this is somewhat my first time doing something like this. I’m also not sure how functional this is, considering the only script I tested it on was the TestCode module seen below.

The code:

local sandBox = { };
local proxy = { };
local oldInstance = Instance;

local allowedGlobalEnvironment = {
	Instance = {
		new = function(className, parent)
			return proxy:create(oldInstance.new(className, parent));
		end;
	};
	
	assert = assert;
	collectgarbage = collectgarbage;
	error = error;
	_G = _G;
	gcinfo = gcinfo;
	getmetatable = getmetatable;
	ipairs = ipairs;
	loadstring = loadstring;
	newproxy = newproxy;
	next = next;
	os = os;
	pairs = pairs;
	pcall = pcall;
	print = print;
	select = select;
	setmetatable = setmetatable;
	tonumber = tonumber;
	tostring = tostring;
	type = type;
	unpack = unpack;
	_VERSION = _VERSION;
	xpcall = xpcall;
	Delay, delay = Delay, delay;
	ElapsedTime = ElapsedTime;
	LoadLibrary = LoadLibrary;
	printidentity = printidentity;
	require = require;
	Spawn = Spawn;
	tick = tick;
	time = time;
	version, Version = version, Version;
	wait, Wait = wait, Wait;
	warn = warn;
	ypcall = ypcall;
};

--============================================================================================--

proxy.Proxies = { };

proxy.BlackList = {
	[{
		ClassNames = { 'BasePart', 'BaseScript' };
		LockedProperties = { 'Parent', 'Name' };
		BlockedFunctions = { 'Destroy', 'ClearAllChildren' };
	}] = function(self, index)
		if type(self[index]) == 'function' then
			warn(string.format('Attempted to use %s (a protected function)', self:GetFullName() .. '::' .. index));
		else
			warn(string.format('Attempted to edit %s (a protected property)', self:GetFullName() .. '.' .. index));
		end;
	end;
};

function proxy:create(object)
	local newObj = newproxy(true);
	local newObjMT = getmetatable(newObj);
	
	function newObjMT.__index(self, index)
		local value = object[index];
		
		if type(value) == 'userdata' then
			return proxy:get(value);
		elseif type(value) == 'table' then
			local ret = { };
			
			for ind, val in next, value do
				ret[ind] = proxy:get(val);
			end
			
			return ret;
		elseif type(value) == 'function' then
			return function(self, ...)
				-- Check if we're trying to call a blacklisted method
				-- on a specific baseclass. If we are, we need to
				-- override it with our own function.
				
				for tbl, overrideFunc in next, proxy.BlackList do
					for _, oClass in next, tbl.ClassNames do
						for _, fName in next, tbl.BlockedFunctions do
							if object:IsA(oClass) and string.lower(fName) == string.lower(index) then
								overrideFunc(object, index);
								return 0;
							end;
						end;
					end;
				end;
				
				return proxy:get(object[index](self, ...));
			end;
		else
			return value;
		end
	end;
	
	function newObjMT.__newindex(self, index, value)
		--LockedProperties
		for tbl, overrideFunc in next, proxy.BlackList do
			for _, oClass in next, tbl.ClassNames do
				for _, pName in next, tbl.LockedProperties do
					if object:IsA(oClass) and string.lower(pName) == string.lower(index) then
						overrideFunc(object, index);
						return 0;
					end;
				end;
			end;
		end;
		
		object[index] = proxy:get(value);
	end;
	
	newObjMT.__metatable = 'The metatable is locked';
	
	function newObjMT.__tostring(...)
		return tostring(object);
	end;
	
	proxy.Proxies[object] = newObj;
	
	return newObj;
end

function proxy:get(object)
	local got_object = proxy.Proxies[object];
	
	if got_object then
		return got_object;
	else
		if type(object) == 'userdata' then
			return proxy:create(object);
		else
			return object;
		end;
	end
end

--============================================================================================--

local addToGlobalEnvironment = {
	[{ 'Game', 'game' }] = proxy:get(Game);
	[{ 'Workspace', 'workspace' }] = proxy:get(Workspace);
	[{ 'script' }] = proxy:get(script);
}

for varTbl, val in next, addToGlobalEnvironment do
	for _, var in next, varTbl do
		allowedGlobalEnvironment[var] = val;
	end
end

setmetatable(allowedGlobalEnvironment, {
	__index = function(self, index)
		local obj = rawget(self, index);
		
		if type(obj) == 'userdata' and proxy.Proxies[obj] then
			obj = proxy:get(obj);
		end;
		
		return obj;
	end;
	
	__newindex = function(self, index, value)
		rawset(self, index, proxy:get(value));
	end
});

--============================================================================================--

function sandBox:Clean(func)
	return setfenv(func, allowedGlobalEnvironment);
end;

function sandBox:Execute(module)
	local f = require(module);
	
	sandBox:Clean(f);
	
	local didNotError, stringError = ypcall(f);
	
	return not didNotError and false, stringError or true;
end;

--============================================================================================--

_G.SandBox = sandBox;

local ran, err = _G.SandBox:Execute(script.TestModule);
if not ran then print(err); end;

TestModule code:

return function()
Instance.new(‘Part’):Destroy();
Workspace.BasePlate:Destroy();
Workspace.BasePlate.Name = ‘lel’;
getfenv(print);
end;

The result:

23:21:22.176 - Attempted to use Part::Destroy (a protected function)
23:21:22.177 - Attempted to use Workspace.BasePlate::Destroy (a protected function)
23:21:22.178 - Attempted to edit Workspace.BasePlate.Name (a protected property)
ServerScriptService.TestCode:5: attempt to call global 'getfenv' (a nil value)
2 Likes

I think I see a Gariblox Mod coming to ROBLOX.

1 Like