Modify Lua interpreter to implement a sandbox script language

classic Classic list List threaded Threaded
12 messages Options
Reply | Threaded
Open this post in threaded view
|

Modify Lua interpreter to implement a sandbox script language

Brice André
Dear all,

I am trying to embed Lua for a sandbox scripting language, where all potentially harmful functions would be deactivated.

To do so, I patched the file "linit.c" and commented all lines of "loadedlibs" declaration:

static const luaL_Reg loadedlibs[] = {
 // {"_G", luaopen_base},
 // {LUA_LOADLIBNAME, luaopen_package},
 // {LUA_COLIBNAME, luaopen_coroutine},
 // {LUA_TABLIBNAME, luaopen_table},
 // {LUA_IOLIBNAME, luaopen_io},
 // {LUA_OSLIBNAME, luaopen_os},
 // {LUA_STRLIBNAME, luaopen_string},
 // {LUA_MATHLIBNAME, luaopen_math},
 // {LUA_UTF8LIBNAME, luaopen_utf8},
 // {LUA_DBLIBNAME, luaopen_debug},
#if defined(LUA_COMPAT_BITLIB)
 // {LUA_BITLIBNAME, luaopen_bit32},
#endif
  {NULL, NULL}
};

As a result, functions like 'io.open' are no more available. But I am a little puzzled because some functiosn declared in "luaopen_base", like "print" function, are still available.

A I doing something wrong, or am I missing something ?

Or maybe is there a simpler/safer way of achieving what I am tring to do ?

Thanks in advance for your help,

Brice
Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Leonardo Gomes
If you control the code that loads the untrusted Lua script and don't intend on modifying lua itself, I think you can achieve a "sandboxed" environment by using the setfenv function.

You could load the untrusted script through something like

chunk = loadfile("script.lua")
setfenv(chunk, {/* in this table you place only the functions that you don't consider harmful */ print = print})
script_result = chunk()

Em Seg, 3 de set de 2018 16:12, Brice André <[hidden email]> escreveu:
Dear all,

I am trying to embed Lua for a sandbox scripting language, where all potentially harmful functions would be deactivated.

To do so, I patched the file "linit.c" and commented all lines of "loadedlibs" declaration:

static const luaL_Reg loadedlibs[] = {
 // {"_G", luaopen_base},
 // {LUA_LOADLIBNAME, luaopen_package},
 // {LUA_COLIBNAME, luaopen_coroutine},
 // {LUA_TABLIBNAME, luaopen_table},
 // {LUA_IOLIBNAME, luaopen_io},
 // {LUA_OSLIBNAME, luaopen_os},
 // {LUA_STRLIBNAME, luaopen_string},
 // {LUA_MATHLIBNAME, luaopen_math},
 // {LUA_UTF8LIBNAME, luaopen_utf8},
 // {LUA_DBLIBNAME, luaopen_debug},
#if defined(LUA_COMPAT_BITLIB)
 // {LUA_BITLIBNAME, luaopen_bit32},
#endif
  {NULL, NULL}
};

As a result, functions like 'io.open' are no more available. But I am a little puzzled because some functiosn declared in "luaopen_base", like "print" function, are still available.

A I doing something wrong, or am I missing something ?

Or maybe is there a simpler/safer way of achieving what I am tring to do ?

Thanks in advance for your help,

Brice
Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Brice André
Hello Leonardo,

Thanks for your suggestion, which seems very interesting. I am a bit hesitant to go this way because I would be more confident in a lua version that is compiled with no potentially harmful functions, than to try to prevent to execute them at runtime. But maybe am I wrong on that point.

I will perform some tests with your solution.

But in all cases, I would be very interested to understand how a lua interpreter compiled without "luaopen_base" library initialisation call could still perform print function. For other functions I tested, this solution seemed to be concluant. But for this very particular function, my technique seems not to work. I do not really need to deactivate print function, as it is not really harmful, but I would be more confident in the technique if I understand why it does not behaves like I would have expected.

Any idea ?

Regards,

Brice
 

2018-09-03 21:21 GMT+02:00 Leonardo Gomes <[hidden email]>:
If you control the code that loads the untrusted Lua script and don't intend on modifying lua itself, I think you can achieve a "sandboxed" environment by using the setfenv function.

You could load the untrusted script through something like

chunk = loadfile("script.lua")
setfenv(chunk, {/* in this table you place only the functions that you don't consider harmful */ print = print})
script_result = chunk()

Em Seg, 3 de set de 2018 16:12, Brice André <[hidden email]> escreveu:
Dear all,

I am trying to embed Lua for a sandbox scripting language, where all potentially harmful functions would be deactivated.

To do so, I patched the file "linit.c" and commented all lines of "loadedlibs" declaration:

static const luaL_Reg loadedlibs[] = {
 // {"_G", luaopen_base},
 // {LUA_LOADLIBNAME, luaopen_package},
 // {LUA_COLIBNAME, luaopen_coroutine},
 // {LUA_TABLIBNAME, luaopen_table},
 // {LUA_IOLIBNAME, luaopen_io},
 // {LUA_OSLIBNAME, luaopen_os},
 // {LUA_STRLIBNAME, luaopen_string},
 // {LUA_MATHLIBNAME, luaopen_math},
 // {LUA_UTF8LIBNAME, luaopen_utf8},
 // {LUA_DBLIBNAME, luaopen_debug},
#if defined(LUA_COMPAT_BITLIB)
 // {LUA_BITLIBNAME, luaopen_bit32},
#endif
  {NULL, NULL}
};

As a result, functions like 'io.open' are no more available. But I am a little puzzled because some functiosn declared in "luaopen_base", like "print" function, are still available.

A I doing something wrong, or am I missing something ?

Or maybe is there a simpler/safer way of achieving what I am tring to do ?

Thanks in advance for your help,

Brice

Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Andrew Gierth
In reply to this post by Brice André
>>>>> "Brice" == Brice André <[hidden email]> writes:

 Brice> Dear all,

 Brice> I am trying to embed Lua for a sandbox scripting language, where
 Brice> all potentially harmful functions would be deactivated.

How do you define "harmful" for your environment? And what features do
you want sandboxed code to be able to use?

 Brice> To do so, I patched the file "linit.c"

You shouldn't do it that way as a general rule.

There are a few different approaches: you can simply not use
luaL_openlibs() at all; or you can remove the unwanted libraries before
calling any untrusted code; or you can use a separate environment table
for untrusted code and populate it by copying only the functions and
(copies of) library tables you want to allow. (This last method is the
most flexible but probably also the most work to do.)

 Brice> As a result, functions like 'io.open' are no more available. But
 Brice> I am a little puzzled because some functiosn declared in
 Brice> "luaopen_base", like "print" function, are still available.

That suggests that you called luaopen_base yourself from somewhere.

Some of the base functions are essentially part of the language -
especially select(), pairs(), ipairs(), type(), pcall(), error(),
assert(), tonumber(), tostring(). Without at least those, writing any
nontrivial code will be hard.

--
Andrew.

Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Andrew Gierth
In reply to this post by Leonardo Gomes
>>>>> "Leonardo" == Leonardo Gomes <[hidden email]> writes:

 Leonardo> If you control the code that loads the untrusted Lua script
 Leonardo> and don't intend on modifying lua itself, I think you can
 Leonardo> achieve a "sandboxed" environment by using the setfenv
 Leonardo> function.

setfenv doesn't exist in 5.2+; in current versions you use the
environment parameter to load() instead.

--
Andrew.

Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Brice André
In reply to this post by Andrew Gierth
Thanks for your answer.

Andrew> How do you define "harmful" for your environment? And what features do
Andrew> you want sandboxed code to be able to use?

I want to prevent the script from reading or modifying something on the computer it is running. So, no access to file system, no access to sockets, etc. I am not interested in preventing denial of service. So 100% CPU usage, memory exhaution, etc. is not a real issue for me.

The script must be able to perform basic language stuff (assigning/reading variables, structure control (if-then-else, loops, etc.), string manipulation, etc. It will retrieve its inputs and provide its outputs from dedicated functions written in another language and explicitly provided to the script (C functions registered by lua_register).

Andrew> Some of the base functions are essentially part of the language -
Andrew> especially select(), pairs(), ipairs(), type(), pcall(), error(),
Andrew> assert(), tonumber(), tostring(). Without at least those, writing any
Andrew> nontrivial code will be hard.

I agree with you. My idea was first to remove everything, test it, and re-enable non-harmfull functions after. As I was struggling on print function, I tought there was a big issue in my approach.

By the way, I found why 'print' is still available (and why this is the only function of base library that is available). I embed this modified lua engine in an existing library (php-lua) that I did not write myself. After some checks, I realised that this library is exporting its own print function at init, and this is this function that is available (and not the one in lua base library..).

So, in conclusion, I think I will keep the patched version of lua engine because I am more confident with that. But, in addition, I will also put in place your suggestion of using a separate environment, just as a second countermeasure in case of...

If you see anything else that I missed, do not hesitate to comment.

Thanks for your help,
Brice

2018-09-03 22:03 GMT+02:00 Andrew Gierth <[hidden email]>:
>>>>> "Brice" == Brice André <[hidden email]> writes:

 Brice> Dear all,

 Brice> I am trying to embed Lua for a sandbox scripting language, where
 Brice> all potentially harmful functions would be deactivated.

How do you define "harmful" for your environment? And what features do
you want sandboxed code to be able to use?

 Brice> To do so, I patched the file "linit.c"

You shouldn't do it that way as a general rule.

There are a few different approaches: you can simply not use
luaL_openlibs() at all; or you can remove the unwanted libraries before
calling any untrusted code; or you can use a separate environment table
for untrusted code and populate it by copying only the functions and
(copies of) library tables you want to allow. (This last method is the
most flexible but probably also the most work to do.)

 Brice> As a result, functions like 'io.open' are no more available. But
 Brice> I am a little puzzled because some functiosn declared in
 Brice> "luaopen_base", like "print" function, are still available.

That suggests that you called luaopen_base yourself from somewhere.

Some of the base functions are essentially part of the language -
especially select(), pairs(), ipairs(), type(), pcall(), error(),
assert(), tonumber(), tostring(). Without at least those, writing any
nontrivial code will be hard.

--
Andrew.

Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Dirk Laurie-2
In reply to this post by Andrew Gierth
Op Ma., 3 Sep. 2018 om 22:03 het Andrew Gierth
<[hidden email]> geskryf:

> Some of the base functions are essentially part of the language -
> especially select(), pairs(), ipairs(), type(), pcall(), error(),
> assert(), tonumber(), tostring(). Without at least those, writing any
> nontrivial code will be hard.

In fact, without tostring, print() does not work. (Monkey-patching
print by changing tostring is an amusing pastime.)

I would add the string library to "essentially part of the language",
but you can't hide that without invoking debug.setmetatable.

Three of the items on the list are basically dispensible, though.

select('#',...) can't be done in another way, but select(n,...) is
equivalent to {...}[n] (slower, of course, but a sandbox is ... well,
filled with sand, so not the ideal place for running fast).

pairs(tbl) is equivalent to next,tbl which uses one keystroke less,
except that pairs honours the __pairs metamethod.

"for k,v in ipairs(tbl) do" is equivalent to "for k=1,1e400 do local
v=tbl[k] if v==nil then break end" which is much longer.

If you allow me to keep string metamethods, tonumber can be avoided too.

Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Andrew Gierth
>>>>> "Dirk" == Dirk Laurie <[hidden email]> writes:

 >> Some of the base functions are essentially part of the language -
 >> especially select(), pairs(), ipairs(), type(), pcall(), error(),
 >> assert(), tonumber(), tostring(). Without at least those, writing
 >> any nontrivial code will be hard.

 Dirk> In fact, without tostring, print() does not work.
 Dirk> (Monkey-patching print by changing tostring is an amusing
 Dirk> pastime.)

In my sandbox, print() does not call _G.tostring() :-)

 Dirk> I would add the string library to "essentially part of the
 Dirk> language", but you can't hide that without invoking
 Dirk> debug.setmetatable.

If you don't call luaopen_string then the string metatable is never set
up.

In my case I have no need to hide it, but I _do_ need to protect the
metatable itself from tampering, so I do the equivalent of

  getmetatable("").__metatable = true

--
Andrew.

Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Frank Kastenholz-2
In reply to this post by Brice André

Hi
The simple answer is that you have to track down each and every function and then do the appropriate thing to disable/remove it.
Removing the code also is the only method that guarantees that the code can not be invoked via some unforeseen path.

Doing things like playing with the environment at runtime (eg “print=function () return end” or somesuch) certainly work and may be easier than modifying the base lua interpreter and libraries — on the other hand, some environments/project requirements/etc might only accept the  absolute certainty that comes from physically removing the problematic code.

> On Sep 3, 2018, at 3:12 PM, Brice André <[hidden email]> wrote:
>
> Dear all,
>
> I am trying to embed Lua for a sandbox scripting language, where all potentially harmful functions would be deactivated.
>
> To do so, I patched the file "linit.c" and commented all lines of "loadedlibs" declaration:
>
> static const luaL_Reg loadedlibs[] = {
>  // {"_G", luaopen_base},
>  // {LUA_LOADLIBNAME, luaopen_package},
>  // {LUA_COLIBNAME, luaopen_coroutine},
>  // {LUA_TABLIBNAME, luaopen_table},
>  // {LUA_IOLIBNAME, luaopen_io},
>  // {LUA_OSLIBNAME, luaopen_os},
>  // {LUA_STRLIBNAME, luaopen_string},
>  // {LUA_MATHLIBNAME, luaopen_math},
>  // {LUA_UTF8LIBNAME, luaopen_utf8},
>  // {LUA_DBLIBNAME, luaopen_debug},
> #if defined(LUA_COMPAT_BITLIB)
>  // {LUA_BITLIBNAME, luaopen_bit32},
> #endif
>   {NULL, NULL}
> };
>
> As a result, functions like 'io.open' are no more available. But I am a little puzzled because some functiosn declared in "luaopen_base", like "print" function, are still available.
>
> A I doing something wrong, or am I missing something ?
>
> Or maybe is there a simpler/safer way of achieving what I am tring to do ?
>
> Thanks in advance for your help,
>
> Brice


Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

szbnwer@gmail.com
In reply to this post by Dirk Laurie-2
hi all :)

Il giorno mar 4 set 2018 alle ore 08:21 Dirk Laurie
<[hidden email]> ha scritto:
> select('#',...) can't be done in another way, but select(n,...) is
> equivalent to {...}[n]

select() and {...} are not really identical... i have a safe
environment to run strings via evaluating and pcall()'ing them, but
when ive set up buttons with onClick methods, then i had to face with
the fact that they are new calls out of the scope of the original
pcall even if theyve bornt inside that. so my app could crash during
experimenting ... this led to a handsome function wrapper as follows:

-- wrapper function that handles errors
-- ProtectFunction(fun)(...) -- flush output
-- protectFunction(fun)(...) -- don't flush output
-- invisible if all good
-- returns nil and error message on fail, and pretty prints it out via outErr
do
  local pf=function(flushOutput) -- internal setup only
    return function(f) -- the actual wrapper
      return function(...) -- the new function
        return (function(...) -- results from pcall below
          if ... then -- succ
            return select(2, ...)
          else -- err
            outErr(select(2, ...))
            if flushOutput then writeOutput(app) end
            return nil, select(2, ...) end end)(pcall(f, ...)) end end end
  ProtectFunction=pf(true)
  protectFunction=pf(false) end
-- test:
-- Out(ProtectFunction(function(...) return ... end)(nil, ' HAPPY ', nil))
-- Out(protectFunction(function() error'SAD' end)())

here, the additional complexity of the inner most function, and the
test cases are exists cuz the {...} (or whatever else ive tried)
cutted off the trailing nils when i wanted to use that solution
previously, but a vararg function doesnt :) so when ive printed out
the results from some random tests, then ive seen that trailing nils
are getting lost, but only this way not, and finally ive felt like i
must over-comment it for the late future instead of let it stay in the
"dont touch magic" state; and whenever my stuff gonna become public
and any brave volunteer will try to simplify it, the test cases will
tell them the truth :D i believe that overengineered mystical gems can
exists as long as they are finalized and well documented, so no1
should really touch them again that way :D if it would use {...} then
maybe 1 day someone will need to touch it again, or it could make
mystical errors... ive already hit the case that revealed the
difference, so its possible to meet with it again...

if any1 believes that their tools from a given sandbox is identical
with the original behavior, then such small differences can make them
really hate the creator ;) (fine ... except if they are well
documented)

ps: yep, i love factories! :D they are so much recreational ... and
useful as well :)

bests for all! :)

Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Brice André
In reply to this post by Andrew Gierth
Andrew> setfenv doesn't exist in 5.2+; in current versions you use the
Andrew> environment parameter to load() instead.

I tried the "sandbox enviroment" method you propopsed. After some googling, I tried the following code (with call to "PushSandboxEnvironmentTable" inserted between the "luaL_loadbuffer" and the "lua_pcall" to set a local environment with only function "print" available.

But at execution with this code, lua is complaining that "print" does not exist.

Any idea of what is wrong with this approach ?

Regards,

Brice


static void PushSandboxEnvironmentTableEntry(lua_State* L , char* key , char* value) {
    lua_pushstring(L, key);
    lua_pushstring(L, value);
    lua_settable(L, -3);
}

static int PushSandboxEnvironmentTable(lua_State* L)
{
    lua_newtable(L);
    PushSandboxEnvironmentTableEntry(L, "print", "print");
    lua_setupvalue(L, -2, 1);
    return LUA_OK;
}


2018-09-03 22:05 GMT+02:00 Andrew Gierth <[hidden email]>:
>>>>> "Leonardo" == Leonardo Gomes <[hidden email]> writes:

 Leonardo> If you control the code that loads the untrusted Lua script
 Leonardo> and don't intend on modifying lua itself, I think you can
 Leonardo> achieve a "sandboxed" environment by using the setfenv
 Leonardo> function.

setfenv doesn't exist in 5.2+; in current versions you use the
environment parameter to load() instead.

--
Andrew.


Reply | Threaded
Open this post in threaded view
|

Re: Modify Lua interpreter to implement a sandbox script language

Andrew Gierth
>>>>> "Brice" == Brice André <[hidden email]> writes:

 Andrew> setfenv doesn't exist in 5.2+; in current versions you use the
 Andrew> environment parameter to load() instead.

 Brice> I tried the "sandbox enviroment" method you propopsed. After
 Brice> some googling, I tried the following code (with call to
 Brice> "PushSandboxEnvironmentTable" inserted between the
 Brice> "luaL_loadbuffer" and the "lua_pcall" to set a local environment
 Brice> with only function "print" available.

 Brice> But at execution with this code, lua is complaining that "print"
 Brice> does not exist.

It ought to be complaining that "print" is not a function, because what
you pushed in the environment table is a string not a closure.

If you want to see my actual code, then:

https://github.com/RhodiumToad/pllua-ng/blob/master/src/trusted.c
(this file is the one setting up the sandbox environment)

(also see compile.c:pllua_prepare_function which actually sets the
environment upvalue, though in my case it's not supplying the sandbox
table directly but rather a metatable whose __index points to the
sandbox's "global" table; some code in init.c, elog.c and error.c may
also be relevant.)

--
Andrew.