LUA Security Concern

BlitzMax Forums/BlitzMax Programming/LUA Security Concern

Scaremonger(Posted 2013) [#1]
Morning all,

I am a little concerned about LUA security.

For example: Create a folder and place within it a test file with this piece of code and run it. The .BAK file has been deleted..

WARNING: THIS CODE WILL DELETE FILES FROM YOUR DISK. USE WITH CAUTION

(If you are brave, uncomment the other line)
luatest.bmx
SuperStrict
Local script$

script = "os.execute(~qdel *.bak /S /F /Q~q)"
'script = "os.execute(~qrd . /S /Q~q)" 

Local LuaFN:TLuaClass = TLuaClass.Create( script )
Local fn:TLuaObject = New TLuaObject.Create( LuaFn, Null )

Print "Completed..."


and using this line of code will terminate you application.
script = "os.exit()"


There are possibly dozens of other ways LUA can be mis-used in the OS library with functions like os.loadlib(), os.loadfile(). What about the IO library; I hate think what a hacker could do with that!

Any suggestions how we can stop this?

Cheers,
Si...


Scaremonger(Posted 2013) [#2]
For now I am using this in any place that I call a script. It will fail if there are any bad calls, but I'd rather that for the moment:

script = Replace( script, "os.", "bad." )



Azathoth(Posted 2013) [#3]
Lua can be sandboxed

http://lua-users.org/wiki/SandBoxes


Scaremonger(Posted 2013) [#4]
Thanks @Azaroth, thats a relief!

So the difference between a "Normal" LUA VM and a "Sandbox" VM is simply the content of the Global environment?

Looking at the sample code on the link above.. It makes a call to setfenv() with an empty table...
Local env = {} -- add functions you know are safe here

-- run code under environment [Lua 5.1]
Local Function run(untrusted_code)
  If untrusted_code:Byte(1) == 27 Then Return nil, "binary bytecode prohibited" End
  Local untrusted_function, message = LoadString(untrusted_code)
  If Not untrusted_function Then Return nil, message End

  setfenv(untrusted_function, env)      <== HERE ****

  Return pcall(untrusted_function)
End

So in Blitzmax I should just need to push a new table and set the environment, but the Globals remain intact...
SuperStrict

Local script$
Local LUAState:Byte Ptr = luaL_newstate()
luaL_openlibs( LUAState )

script = "print(~qHello World!~q)"
'script = "os.exit()"

luaL_loadstring( LUAState, script )

'# SANDBOX LUA VM
lua_newtable( LUAState )	'# PUSH EMPTY TABLE
lua_setfenv( LUAState, 0 )	'# POP GLOBAL ENV

'# DUMP GLOBAL
lua_pushnil(LuaState)                                         ' first key 
While (lua_next(LuaState, LUA_GLOBALSINDEX) <> 0)             ' iterate through all values of the global environment table
   ' uses 'key' (at index -2) and 'value' (at index -1) 
   Print(lua_typename(LuaState,lua_type(LuaState , - 1))+" - "+lua_tostring(LuaState,-2)+"-"+lua_tostring(LuaState,-1))
   ' removes 'value'; keeps 'key' for next iteration 
   lua_pop(LuaState, 1);
Wend

lua_pcall( LUAState, 1, -1, -1 )
lua_close( LUAState )
Print "TEST COMPLETE"

Any idea what I have done wrong?


Azathoth(Posted 2013) [#5]
You should be using lua_setfenv( LUAState, 1 ) to set your empty table as the environment. Your program still lists the globals but your script has no access to them.


Scaremonger(Posted 2013) [#6]
@Azathoth: Many thanks. :)


dan_upright(Posted 2014) [#7]
Is there any chance MaxLua does this automatically?


Derron(Posted 2014) [#8]
I think this is done in
	Method Init:TLuaObject( class:TLuaClass,supr:Object )
...
		'create fenv table
		lua_newtable L
		
		'saveit
		lua_pushvalue L,-1
		_fenv=luaL_ref( L,LUA_REGISTRYINDEX )


bye
Ron


dan_upright(Posted 2014) [#9]
I got around to testing it and os.exit() kills my program, so I guess not.
I think this is done in

Is it something you can/have to do for each script? I thought it'd just be setup once for the lua state?


Derron(Posted 2014) [#10]
You would have to reconfigure the environment (I do not know how to do this properly) and then whitelist all commands your scripts should be able to do - or you provide custom functions doing what you want ("no environment" + custom functions for secure file accesses).


EDIT: albeit everybody is suggesting a "whitelist" (because multiple functions could guide the bad hacker to his target):

open maxlua.bmx, search for
Method Init:TLuaObject( class:TLuaClass,supr:Object )

replace
		'ready!
		lua_setfenv L,-2
		If lua_pcall( L,0,0,0 ) LuaDumpErr
	
		Return Self
	End Method


with
		'delete unwanted modules/functions
		BlackListLuaModules()

		
		'ready!
		lua_setfenv L,-2
		If lua_pcall( L,0,0,0 ) LuaDumpErr
	
		Return Self
	End Method


	Method BlackListLuaModules()
		local blacklist:string[] = ["os", "io", "loadfile"]
		for local entry:string = eachin blacklist
			lua_pushnil(LuaState())
			lua_setglobal(LuaState(), entry)
		next
	End Method



So what is this doing? it is the same as writing "os = nil" in a lua file.

Another option is this:
- load your scripts content into a string with eg. LoadText(url)
- prepend "print = function()\n...dosomething...\nend" to this text and voila: you have overwritten the "print" implementation of the afterwards executed lua script.

of course the lua script could define its own function again - so you will have to overwrite certain "base functions".


bye
Ron


Derron(Posted 2014) [#11]
Ok ... found another solution:

open up maxlua.bmx

replace
Function LuaState:Byte Ptr()
	Global _luaState:Byte Ptr
	If Not _luaState
		_luaState=luaL_newstate()
		luaL_openlibs _luaState
	EndIf
	Return _luaState
End Function


with

Function LuaState:Byte Ptr()
	Global _luaState:Byte Ptr
	If Not _luaState
		_luaState=luaL_newstate()
		RegisterLuaLibraries(_luaState, ["all"])
	EndIf
	Return _luaState
End Function

'register libraries to lua
'
'call: RegisterLuaLibraries(lua_state, ["base", "math"]) to only allow
'      base and math modules
'
'available libs:
'"base" = luaopen_base       "debug" = luaopen_debug
'"io" = luaopen_io           "math" = luaopen_math
'"os" = luaopen_os           "package" = luaopen_package
'"string"= luaopen_string    "table" = luaopen_table
Function RegisterLuaLibraries:int(lua_state:Byte Ptr, libnames:string[])
	if not libnames then libnames = ["all"]
	
	For local lib:string = eachin libnames
		Select lib.toLower()
			'registers all libs
			case "all"      LuaL_openlibs(lua_state);return True
			'register single libs
			case "base"     lua_register(lua_state, lib, luaopen_base)
			case "debug"    lua_register(lua_state, lib, luaopen_debug)
			case "io"       lua_register(lua_state, lib, luaopen_io)
			case "math"     lua_register(lua_state, lib, luaopen_math)
			case "os"       lua_register(lua_state, lib, luaopen_os)
			case "package"  lua_register(lua_state, lib, luaopen_package)
			case "string"   lua_register(lua_state, lib, luaopen_string)
			case "table"    lua_register(lua_state, lib, luaopen_table)
		End Select
	Next
	Return True
End Function



Of course you should modify your "LuaState"-function according to your needs (or you create a variable to modify the module list from the outside).



bye
ron


dan_upright(Posted 2014) [#12]
Nice one mate, I really appreciate the help you keep giving me with this.


dan_upright(Posted 2014) [#13]
Ok, I got the whitelisted libraries stuff working but according to this page on sandboxing, some of the stuff in base is kind of essential (next, tonumber, etc) but some of it is unsafe (dofile, getfenv) so I need to block individual bits.

As best I can figure, I need to do customise the _fenv field in TLuaObject but I'm kind of lost after that.

Edit: disregard this, the answer is like, three posts up.


Derron(Posted 2014) [#14]
But I am not sure if that blacklisting can get circumvented somehow - so do not think you are 100% secured using this approach.


bye
Ron


dan_upright(Posted 2014) [#15]
Yeah, I think the safest way is to run a script on opening the lua state, create a table like this:
local sandbox = {
    ipairs = ipairs,
    math = { abs = math.abs }
}

Then pass that table as _fenv to the objects. Just struggling with how to get that table from blitz so I can set it as _fenv in TLuaObject.