LUA Security Concern
BlitzMax Forums/BlitzMax Programming/LUA Security Concern
| ||
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... |
| ||
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." ) |
| ||
Lua can be sandboxed http://lua-users.org/wiki/SandBoxes |
| ||
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? |
| ||
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. |
| ||
@Azathoth: Many thanks. :) |
| ||
Is there any chance MaxLua does this automatically? |
| ||
I think this is done inMethod 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 |
| ||
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? |
| ||
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 |
| ||
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 |
| ||
Nice one mate, I really appreciate the help you keep giving me with this. |
| ||
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. |
| ||
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 |
| ||
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. |