Don't lock the string metatable

I need to set getmetatable(“”).__index for a sandbox and the metatable is locked so now the sandbox is forced to allow access to string.rep with no filter.

My code:

function publicFunc(str)
   assert(type(str)=='string')
   setGameState('TemporaryState')
   if str:find('asdf') then
      ---
   end
   setGameState('PermanentState')
end

Hacker’s code:

hack = ''
getmetatable(hack).__index = error
publicFunc(hack)

In this example my game gets stuck in the temporary state because of a fake string. I’d rather not have to sanitize all my strings or risk breaking my game. The idea of sandboxing lua within lua sounds kind of far-fetched, what are you trying to do?

I’m writing a sandbox that doesn’t crash
http://devforum.roblox.com/t/sandboxing-against-crashes/26536/5

If not unlock the metatable, how can I sandbox string methods to filter things like ("."):rep(1e9)?

You could implement a Lua VM in Lua. There’s already code out there, Something like LuLu might have what you want.

1 Like

I’ve sandboxed strings without using a VM.
It’s very… fun I guess?
Allows me to sandbox (“”).dump and (“abc”):rep(1e99) etc, you get the idea.

don’t mind me not explaining how, I feel like building some suspense in case people want to know

does it let you sandbox things like x="rep"x[x](x,1e9)?

After you do x="rep" the variable ‘x’ is the sandboxed version of “rep”, so yes.
(well I think I was too lazy to actually override :rep(), but since it’s my own “string”, I can)

x=next({rep=true})x[x](x,1e9)

As long as next is sandboxed, which it should be, yes:

local onext = next
function next(...)
	return sandbox(onext(unsandbox(...)))
end

so how are you sandboxing the strings? something like local x="rep" won’t fire newindex because it’s local, and something like (function(x)x[x](x,1e9)end)"rep" could be difficult to replace properly.

Remember the parser you use for the infinite loops?
I mentioned I “parse” the code into strings, comments and actual code.
The nice thing about strings is, you can put brackets around them without an issue.
You can also change the “hi” into wrap"hi" inside the brackets:

print"hi"
print("hi")
print(wrap"hi")

local x = "rep"
local x = ("rep")
local x = (wrap"hi")

func("a",b,[[c]])
func(("a"),b,([[c]]))
func((wrap"a"),b,(wrap[[c]]))

As long as the input source parses without a problem, replacing string this way won’t change anything.
(except instead if getting “a” you get wrap"a" allowing you to sandbox them)
At the top of the script you can have something like local Jsf_dfq_QSDF that nobody can guess.
That way, wrapping strings as Jsf_dfq_QSDF"idk" actually leaves no traces.
(except when it errors, then it’s in the stacktrace, which you could manipulate)
(same for debug.traceback() which you can sandbox to hide internal stuff)

Small problem (assuming you sandbox loadstring, which you should, if it’s available):

loadstring("error'hi'")()
--> [string "error(wrap'hi')"]:1: hi

Easy to fix by sandboxing loadstring properly:

function loadstring(source,name)
	return loadstring(fixSource(source),name or source)
end

That way, that error-code will return [string "error'hi'"]:1:hi thus hiding the wrapping.

1 Like

That’s what I thought, but what about escape characters?

print("hi\"") print("hi\\") print("hi\\\"")

Eh I forgot.
Maybe my string patterns work for escaping those.
Maybe I just start a loop to find the closing quote without the backslash.
I probably just gsubbed " and ' to some random thing and gsubbed them back later on. (baaaaad)
Plenty of ways to work around that issue.

I don’t really dislike the idea of gsubbing things out.

s=s:gsub("\\\\","doublebackslash")
s=s:gsub('\\"',"escapeddoublequote")
s=s:gsub("\\'","escapedsinglequote")
s=s:gsub('".-"',"sandboxstring(%0)")
s=s:gsub("'.-'","sandboxstring(%0)")
s=s:gsub("%[(=*)%[(.-)%]%1%]","sandboxstring([%1[%2]%1])")
s=s:gsub("doublebackslash","\\\\")
s=s:gsub("escapeddoublequote",'\\"')
s=s:gsub("escapedsinglequote","\\'")

nested quotes are still an issue, however.

print(“hel’lo, wo’rld”)

Merging both quotes works:

s=s:gsub('(["\']).-%1',"sandboxstring(%0)")
-- instead of
s=s:gsub('".-"',"sandboxstring(%0)")
s=s:gsub("'.-'","sandboxstring(%0)")

Result:

-- from
print("hel'lo, \" \\' wo'rld")
-- to
print(sandboxstring("hel'lo, \" \\' wo'rld")) 

-- from
print("hel'lo, \" \\' wo'rld" or "abc") 
-- to
print(sandboxstring("hel'lo, \" \\' wo'rld") or sandboxstring("abc")) 

doesn’t work for %[=*%[ type strings.

ugh I need the __len metamethod and roblox doesn’t have it, what do I even do

local s=[=[local x='..'for i=1,100 do print(#x)x=x:rep(#x)end]=]
do
	local illegal=s:match("resourcecheck")or s:match("resourcecount")or s:match("doublebackslash")or s:match("escapeddoublequote")or s:match("escapedsinglequote")or s:match("sandboxstring")or s:match("singlequotemark")or s:match("doublequotemark")or s:match("string[=*]brackets")
	if illegal then
		error("Program contains illegal phrase (\""..illegal.."\")")
	end
end
s=s:gsub("(%W)do(%W)","%1do resourcecheck();%2")
s=s:gsub("(%W)repeat(%W)","%1repeat resourcecheck();%2")
s=s:gsub("^do(%W)","do resourcecheck();%1")
s=s:gsub("^repeat(%W)","repeat resourcecheck();%1")
s=s:gsub("\\\\","doublebackslash")
s=s:gsub('\\"',"escapeddoublequote")
s=s:gsub("\\'","escapedsinglequote")
s=s:gsub([[(['"])(.-)%1]],function(_,x)x=x:gsub("%[(=*)%[","string%1brackets")return'"'..x..'"'end)
s=s:gsub("'(.-)'",function(x)x=x:gsub('"',"doublequotemark"):gsub("%[(=*)%[","string%1brackets")return"'"..x.."'"end)
s=s:gsub("%[(=*)%[(.-)%]%1%]",function(e,x)x=x:gsub("'","singlequotemark"):gsub('"',"doublequotemark")return"["..e.."["..x.."]"..e.."]"end)
s=s:gsub('"(.-)"',function(x)return'sandboxstring("'..x..'")'end)
s=s:gsub("'(.-)'",function(x)return"sandboxstring('"..x.."')"end)
s=s:gsub("%[(=*)%[(.-)%]%1%]",function(x,y)return"sandboxstring(["..x.."["..y.."]"..x.."])"end)
s=s:gsub("singlequotemark","'")
s=s:gsub("doublequotemark",'"')
s=s:gsub("string(=*)brackets","[%1[")
s=s:gsub("doublebackslash","\\\\")
s=s:gsub("escapeddoublequote",'\\"')
s=s:gsub("escapedsinglequote","\\'")
s=[[local resourcecount,resourcecheck,sandboxstring=0;
do
	local error,collectgarbage,setmetatable,getmetatable,type=error,collectgarbage,setmetatable,getmetatable,type
	resourcecheck=function()
		resourcecount=resourcecount+1
		if resourcecount==1e7 then
			error("Program is using too much power")
		end
	end
	local function copy(t)local c={}for k,v in pairs(t)do c[k]=v end return c end
	local env={tostring=tostring,tonumber=tonumber,type=type,unpack=unpack,pcall=pcall,pairs=pairs,ipairs=ipairs,next=next,select=select,string=copy(string),table=copy(table),math=copy(math),coroutine=copy(coroutine),print=print}
	for k,v in pairs(env)do
		if type(k)=='function'then
			env[k]=function(...)
				local t={v(...)}
				for a,b in pairs(t)do
					if type(b)=='string'then t[a]=sandboxstring(b)end
				end
				return unpack(t)
			end
		end
	end
	setfenv(1,env)
	local oldrep=string.rep
	string.rep=function(a,b)if #a*b>1e6 then error("string.rep result too long")end return oldrep(a,b)end
	math.randomseed=nil
	sandboxstring=function(s)
		return setmetatable({},{__len=function()return #s end,__index=function(_,k)
			return function(a,...)
				local t={string[k](s,...)}
				for i=1,#t do if type(t[i])=='string'then t[i]=sandboxstring(t[i])end end
				return unpack(t)
			end
		end})
	end
end;]]..s
print(s)
loadstring(s)()

I remember something about dividing strings somehow?
My parser literally divided the code into strings, comments and actual code.

A parser could look more or less like this:

EDIT: Added code @1waffle1

I think I have it parsing correctly, I just need to define the actual sandboxing function and I don’t seem to be able to return the length of the original string when # is used because Roblox doesn’t have __len.