From e1adc774196f51b63812447e902094584015a98b Mon Sep 17 00:00:00 2001
From: Ulrik Sverdrup <ulrik.sverdrup@gmail.com>
Date: Sun, 14 Mar 2010 20:45:34 +0100
Subject: [PATCH] scripting: Sandbox Lua scripts

Code executed in our Lua runtime has by default access to all builtin
lua functions and modules. These include functions to load lua files
or access the operating system.

As an example, a ruleset or scenario script could execute arbitrary
shell scripts (for example the 'uptime' program to use a harmless
example) using: os.execute("uptime"). Additionally the builtin module
io allows lua code to open and read/write files.

If we can, we should make freeciv rock-solid safe w.r.t scenario
scripts, they should be simple data, without security implications.
Otherwise a server administrator must scrutinize any custom ruleset
and scenarios before installing them. And users could experience
viruses in the form of freeciv scenarios or savegames.

Lua provides a method called "setfenv" that allows the caller to set
the environment a called function executes in. We set up a restricted
environment and execute ruleset/scenario code only inside this. In the
code, this restricted execution is carried out inside
script.c:script_call (which is now the only entry point for user
code).

The setup of the restricted environment uses a whitelist of builtin
symbols (functions, values and modules) that we allow in the scripting
environment, defined in api.pkg, where we also have a comment:

    We want to assure that
    1) The script has no access to the operating system
       (loadfile, os module, io module).
    2) The script can not modify modules that freeciv's script runtime
       uses, for example by diverting error handling routines or similar.
    3) The script can not break out of the sandbox.

I have used this community resource as reference when picking builtins
to whitelist:

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

Notice however that a normal freeciv script needs next to no builtins.
We don't forsee needing class and inheritance programming, so much of
lua's power can be turned off. The whitelist of builtins is thus
small.

The sandbox construction assumes that all parts of our game api are
safe.
---
 server/scripting/api.pkg  |   59 +++++++++++++++++++++++++++++++++++++++++++++
 server/scripting/script.c |   37 ++++++++++++++++++++++++++-
 2 files changed, 94 insertions(+), 2 deletions(-)

diff --git a/server/scripting/api.pkg b/server/scripting/api.pkg
index 2cd8fa3..61bdff9 100644
--- a/server/scripting/api.pkg
+++ b/server/scripting/api.pkg
@@ -344,6 +344,65 @@ module find {
 }
 
 $[
+
+-- return a table with keys from list
+function _freeciv_set (list)
+  local set = {}
+  for _, l in ipairs(list) do set[l] = true end
+  return set
+end
+
+-- This is a whitelist of builtin symbols (functions, values and modules)
+-- that we allow in the scripting environment
+--
+-- We want to assure that
+-- 1) The script has no access to the operating system
+--    (loadfile, os module, io module)
+-- 2) The script can not modify modules that freeciv's script runtime
+--    uses, for example by diverting error handling routines or similar.
+-- 3) The script can not break out of the sandbox.
+_freeciv_lua_whitelist = _freeciv_set {
+  'assert',
+  'error',
+  'ipairs',
+  'next',
+  'pairs',
+  'pcall',
+  'print',
+  'select',
+  'tonumber',
+  'tostring',
+  'type',
+  'unpack',
+  '_VERSION',
+  'xpcall',
+  -- We can not use the modules outside sandbox that we allow here.
+  'math',
+  'string',
+  'table',
+}
+
+_freeciv_script_env = {}
+
+-- Setup sandbox environment
+function _freeciv_script_sandbox_build()
+  -- symbols from whitelisted builtins or from api,
+  -- are transferred to the script environment
+  --
+  -- _freeciv_lua_builtins is setup in script.c:script_init
+  for k, v in pairs(_G) do
+    if not k:match("^_freeciv") and (
+      not _freeciv_lua_builtins[k] or _freeciv_lua_whitelist[k]) then
+      _freeciv_script_env[k] = v
+    end
+  end
+  _freeciv_script_env["_G"] = _freeciv_script_env
+  -- delete these tables
+  _freeciv_lua_builtins = nil
+  _freeciv_lua_whitelist = nil
+end
+
+
 -- Dump the state of user scalar variables to a Lua code string.
 function _freeciv_state_dump()
   local res = ''
diff --git a/server/scripting/script.c b/server/scripting/script.c
index b7b26bc..4ea8166 100644
--- a/server/scripting/script.c
+++ b/server/scripting/script.c
@@ -32,6 +32,14 @@
 
 #include "script.h"
 
+/* Prepare a list of Lua's builtin symbols */
+static const char script_sandbox_prepare[] = \
+  "_freeciv_lua_builtins = {}\n"             \
+  "for k, v in pairs(_G) do\n"               \
+  "  _freeciv_lua_builtins[k] = true\n"      \
+  "end\n";
+
+
 /**************************************************************************
   Lua virtual machine state.
 **************************************************************************/
@@ -129,6 +137,8 @@ static int script_call(lua_State *L, int narg, int nret, const char *code)
   lua_getfield(L, -1, "traceback");
   lua_insert(L, base);
   lua_pop(L, 1); /* Pop debug table */
+  lua_getglobal(L, "_freeciv_script_env");
+  lua_setfenv(L, base+1);
 
   status = lua_pcall(L, narg, nret, base);
   if (status) {
@@ -246,6 +256,15 @@ static void script_callback_push_args(int nargs, va_list args)
 }
 
 /**************************************************************************
+  Put a symbol from the script environment on the top of the stack.
+**************************************************************************/
+static void script_get_script_var(lua_State *L, const char *sym_name)
+{
+  lua_getglobal(L, "_freeciv_script_env");
+  lua_getfield(L, -1, sym_name);
+  lua_remove(L, -2); /* remove env table */
+}
+/**************************************************************************
   Invoke the 'callback_name' Lua function.
 **************************************************************************/
 bool script_callback_invoke(const char *callback_name,
@@ -255,7 +274,7 @@ bool script_callback_invoke(const char *callback_name,
   bool stop_emission = FALSE;
 
   /* The function name */
-  lua_getglobal(state, callback_name);
+  script_get_script_var(state, callback_name);
 
   if (!lua_isfunction(state, -1)) {
     freelog(LOG_ERROR, "lua error: Unknown callback '%s'", callback_name);
@@ -318,9 +337,11 @@ static void script_vars_load(struct section_file *file)
 **************************************************************************/
 static void script_vars_save(struct section_file *file)
 {
+  int status;
   if (state) {
     lua_getglobal(state, "_freeciv_state_dump");
-    if (lua_pcall(state, 0, 1, 0) == 0) {
+    status = script_call(state, 0, 1, NULL);
+    if (status == 0) {
       const char *vars;
 
       vars = lua_tostring(state, -1);
@@ -385,6 +406,7 @@ static void script_code_save(struct section_file *file)
 **************************************************************************/
 bool script_init(void)
 {
+  int status;
   if (!state) {
     state = lua_open();
     if (!state) {
@@ -393,6 +415,8 @@ bool script_init(void)
 
     luaL_openlibs(state);
 
+    luaL_dostring(state, script_sandbox_prepare);
+
     tolua_api_open(state);
 
     script_code_init();
@@ -400,6 +424,15 @@ bool script_init(void)
 
     script_signals_init();
 
+    lua_getglobal(state, "_freeciv_script_sandbox_build");
+    status = lua_pcall(state, 0, 0, 0);
+    if (status) {
+      script_report(state, status, 0);
+      lua_close(state);
+      state = NULL;
+      freelog(LOG_FATAL, "Failed to set up script sandbox");
+      return FALSE;
+    }
     return TRUE;
   } else {
     return FALSE;
-- 
1.7.0

