Please take a few minutes to complete this short survey on service testing.

Scripting Go: Putting Lua Behind Bars

  • 2017-03-30

In the previous article I showed you how we can run lua scripts inside Go applications. In real world scenarios you'd want to put in more effort to make sure that we are guarded against rogue scripts.

A rogue is a script that:

  • Runs some commands that should not be allowed.
  • Or it steals resources from the main application.

For example, if we're allowing third parties to pass arbitrary scripts to our application, we may want to ensure that this script has no access to networking or system libraries.

Another example of a malicious script, is a script that runs indefinitely. There could be an error in a loop causing the script to never finish. We may also want to ensure that we're having more control around how much memory can be allocated in the VM.

These are the topics I am going to cover in this post.

Restricting access to modules and functions

Let's say we have this script:

print("hello from jail")

for k, v in pairs(_G) do
    print(k, v)
end

What this script does it prints one line to the screen and then prints the global environment. If we run this script we may see something similar to this.

hello from jail
_VERSION    GopherLua 0.1
error   function: 0xc420014880
load    function: 0xc420014980
channel table: 0xc420068660
next    function: 0xc420014480
_printregs  function: 0xc420014600
assert  function: 0xc4200146c0
_G  table: 0xc4200680c0
dofile  function: 0xc420014580
tostring    function: 0xc420014940
debug   table: 0xc420068600
coroutine   table: 0xc420068720
module  function: 0xc420014540
xpcall  function: 0xc420014780
unpack  function: 0xc420014a80
getfenv function: 0xc420014ac0
io  table: 0xc420068360
getmetatable    function: 0xc4200145c0
table   table: 0xc420068300
setfenv function: 0xc420014500
tonumber    function: 0xc420014800
select  function: 0xc420014a00
string  table: 0xc420068540
math    table: 0xc4200685a0
ipairs  function: 0xc420014b40
pairs   function: 0xc420014bc0
type    function: 0xc420014a40
setmetatable    function: 0xc420014640
rawequal    function: 0xc420014700
rawset  function: 0xc420014840
pcall   function: 0xc4200148c0
require function: 0xc420014680
loadstring  function: 0xc4200147c0
loadfile    function: 0xc4200149c0
package table: 0xc420068180
print   function: 0xc4200144c0
collectgarbage  function: 0xc420014900
rawget  function: 0xc420014740
os  table: 0xc4200684e0

As you can see there's a bunch of modules loaded. Here _G is the global environment table which tells what's available to you script. Lua is really awesome because it allows us to easily change this table. By changing this table we can expose new functionality to the script or make certain functionality unavailable.

Let's say we want to make only print function available and nothing else. We're gonna start by writing our own print implementation in Go. I'm going to borrow one from Gopher-Lua standard lib.

func jailPrint(L *lua.LState) int {
    top := L.GetTop()
    fmt.Printf("%d ", time.Now().Unix())
    for i := 1; i <= top; i++ {
        fmt.Print(L.ToStringMeta(L.Get(i)).String())
        if i != top {
            fmt.Print("\t")
        }
    }
    fmt.Println("")
    return 0
}

This function follows the LGFunction signature. When we call a function, the parameters are passed via stack, we're just popping everything from the stack and printing them out. I also prefix each line with a timestamp. We return 0 because we didn't push anything onto the stack (print returns nothing).

Next we have to define symbol table that we want to export and we need a helper function to set the global environment.

var jailFunctions = map[string]lua.LGFunction{
    "print": jailPrint,
}

func openBase(L *lua.LState) int {
    global := L.Get(lua.GlobalsIndex).(*lua.LTable)
    L.SetGlobal("_G", global)
    basemod := L.RegisterModule("_G", jailFunctions)
    L.Push(basemod)
    return 1
}

Here openBase looks up where the global environment lives in the VM and exports functions under _G in that location.

func newState(jail bool) *lua.LState {
    if jail == false {
        return lua.NewState()
    }

    L := lua.NewState(lua.Options{
        SkipOpenLibs: true,
    })

    if err := L.CallByParam(lua.P{
        Fn:      L.NewFunction(openBase),
        NRet:    0,
        Protect: true,
    }, lua.LString(lua.BaseLibName)); err != nil {
        panic(err)
    }

    return L
}

We have our own print function defined and we have export table ready. The newState function is a helper function that allocates new Lua VM. When we're creating a new state we specify that we should not load any libraries and we want to construct global environment ourselves. If we run our lua script again we're going to see the output of the first print statement and an exception because we no longer have pairs available.

1490920947 hello from jail
panic: jail-1.lua:3: attempt to call a non-function object
stack traceback:
    jail-1.lua:3: in main chunk
    [G]: ?

Controlling memory and CPU

Gopher-Lua makes it really easy for us to guard against long running scripts. We can pass in a Context structure to cancel long running scripts.

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
L.SetContext(ctx)

Any script running for more than a second will throw an exception. An infinite loop script such as this one

while true do print("running...") end

Would give us a timeout error:

1490921257 running...
1490921257 running...
1490921257 running...
1490921257 running...
panic: jail-2.lua:1: context deadline exceeded
stack traceback:
    jail-2.lua:1: in main chunk
    [G]: ?

We can also control VM stack and heap size when creating new VM.

lua.NewState(lua.Options{
    CallStackSize: 120,
    RegistrySize:  120*20,
})

Gopher-Lua has fixed stack and heap, however you may want to control these values depending on the type of tasks you're trying to perform.

We can combine these three techniques to completely isolate our Lua scripts inside Go applications. Entire source code is available on github.

Bluebook - API Testing for Developers

API, end-to-end, and integration testing made simple.

Try Now

Subscribe

Subscribe to stay up to date with the latest content:

Hut for macOS

Design and prototype web APIs and services.

Download Now