やりたいこと
Goの中で安全にユーザーコードを実行したい。GoにはいくつかのLua処理系が移植されているので、Goの空間からLuaを呼び出す方法を試した。なお、この記事は試したこと、気付いたことを書捨てているだけなのであまり参考にしないよう。
わかったこと
Lua処理系は gopher-lua を使った。この処理系はLua 5.1の機能を提供している(一部機能は未実装)。
Go上でのLuaスクリプトの実行は、文字列を読み込んで評価する方法、外部ファイルを読み込んで実行する方法がある。次の2つはどちらも hello
と出力する。
文字列を評価して実行する
L := lua.NewState()
defer L.Close()
if err := L.DoString(`print("hello")`); err != nil {
panic(err)
}
外部ファイルを読み込んで実行する
L := lua.NewState()
defer L.Close()
if err := L.DoFile("hello.lua"); err != nil {
panic(err)
}
ユーザーコードをサンドボックスに閉じ込める
Lua 5.1では env
に利用可能な関数を登録し setfenv()
を利用してユーザーコードを実行することで、信頼されていないコードの実行中に利用可能な機能を制限できる。
-- ここで利用可能な関数を宣言する。 {} は何もなし
local env = {} -- add functions you know are safe here
-- 信頼されないコードを実行するための run関数
local function run(untrusted_code)
-- この辺何やってるかよくわからん…
if untrusted_code:byte(1) == 27 then return nil, "binary bytecode prohibited" end
-- loadstringでユーザーコードの文字列を評価する
local untrusted_function, message = loadstring(untrusted_code)
-- 評価結果が得られなかったら何もしない
if not untrusted_function then return nil, message end
-- setfenvを使用して、ユーザーコード(を評価したもの)に実行可能な関数を定義した env を渡す。
setfenv(untrusted_function, env)
-- ユーザーコードを実行する
return pcall(untrusted_function)
end
実行すると次のようになる。
-- print, debugは定義されていない関数なので実行できない
assert(not run [[print(debug.getinfo(1))]]) --> fails
-- 式はOK
assert(run [[x=1]]) --> ok
assert(run [[while 1 do end]]) --> ok (but never returns)
-- 正常に実行できる
print(run([[a=1]]))
-- エラー終了する
print(run([[print("hello")]]))
Goからどのようにユーザースクリプトを実行するか?
L.DoFile
を使って直接的にファイルを読み込むと、サンドボックス環境の生成前にスクリプトが実行されてしまうため利用できない。Go上でサンドボックスを立ち上げるコードとユーザーコードを連結し、 DoString
で実行するといいかもしれない。
L := lua.NewState()
defer L.Close()
val := "" +
"local env = {print=print}\n" +
"\n" +
"local function run(untrusted_code)\n" +
" if untrusted_code:byte(1) == 27 then return nil, 'binary bytecode prohibited' end\n" +
" local untrusted_function, message = loadstring(untrusted_code)\n" +
" if not untrusted_function then return nil, message end\n" +
" setfenv(untrusted_function, env)\n" +
" return pcall(untrusted_function)\n" +
"end\n"
user_script := "" +
"run [[print 'hello go!']]\n" + // -> hello go!
"run [[print(5.1)]]\n" + // -> 5.1
"run [[print(math.floor(5.1))]]\n" + // -> mathがないので実行されない!!!
"run [[print 'good bye']]" // -> good bye
L.DoString(val + user_script)