Add mod security
authorShadowNinja <shadowninja@minetest.net>
Sat, 6 Sep 2014 00:08:51 +0000 (20:08 -0400)
committerShadowNinja <shadowninja@minetest.net>
Sat, 16 May 2015 22:32:31 +0000 (18:32 -0400)
Due to compatibility concerns, this is temporarily disabled.

22 files changed:
build/android/jni/Android.mk
doc/lua_api.txt
minetest.conf.example
src/defaultsettings.cpp
src/filesys.cpp
src/filesys.h
src/script/common/c_internal.cpp
src/script/cpp_api/CMakeLists.txt
src/script/cpp_api/s_base.cpp
src/script/cpp_api/s_base.h
src/script/cpp_api/s_security.cpp [new file with mode: 0644]
src/script/cpp_api/s_security.h [new file with mode: 0644]
src/script/lua_api/l_mapgen.cpp
src/script/lua_api/l_server.cpp
src/script/lua_api/l_settings.cpp
src/script/lua_api/l_util.cpp
src/script/scripting_game.cpp
src/script/scripting_game.h
src/script/scripting_mainmenu.cpp
src/server.cpp
src/server.h
src/settings.cpp

index 206c30ccf90da9f7b64299e7dd5d2b9df57ab438..f78b78b9bb3e79145580c85c9e19398a56587aba 100644 (file)
@@ -262,6 +262,7 @@ LOCAL_SRC_FILES +=                                \
                jni/src/script/common/c_converter.cpp     \
                jni/src/script/common/c_internal.cpp      \
                jni/src/script/common/c_types.cpp         \
+               jni/src/script/cpp_api/s_async.cpp        \
                jni/src/script/cpp_api/s_base.cpp         \
                jni/src/script/cpp_api/s_entity.cpp       \
                jni/src/script/cpp_api/s_env.cpp          \
@@ -271,8 +272,8 @@ LOCAL_SRC_FILES +=                                \
                jni/src/script/cpp_api/s_node.cpp         \
                jni/src/script/cpp_api/s_nodemeta.cpp     \
                jni/src/script/cpp_api/s_player.cpp       \
+               jni/src/script/cpp_api/s_security.cpp     \
                jni/src/script/cpp_api/s_server.cpp       \
-               jni/src/script/cpp_api/s_async.cpp        \
                jni/src/script/lua_api/l_base.cpp         \
                jni/src/script/lua_api/l_craft.cpp        \
                jni/src/script/lua_api/l_env.cpp          \
index 0cc83bf69a2853127b725d67cee20b7d14bf0a84..2421af069e7a5b7a48d4f59a7cb699fcdb14d711 100644 (file)
@@ -1825,8 +1825,12 @@ Call these functions only at load time!
 
 ### Setting-related
 * `minetest.setting_set(name, value)`
+    * Setting names can't contain whitespace or any of `="{}#`.
+    * Setting values can't contain the sequence `\n"""`.
+    * Setting names starting with "secure." can't be set.
 * `minetest.setting_get(name)`: returns string or `nil`
 * `minetest.setting_setbool(name, value)`
+    * See documentation on `setting_set` for restrictions.
 * `minetest.setting_getbool(name)`: returns boolean or `nil`
 * `minetest.setting_get_pos(name)`: returns position or nil
 * `minetest.setting_save()`, returns `nil`, save all settings to config file
index 4e3e97b957b325dce440da4e90e3c54cd9f9798b..6474289bdbd0ba02d96164bc3f7ee4ca618f5ad8 100644 (file)
 #mgv7_np_cave1 = 0, 12, (100, 100, 100), 52534, 4, 0.5, 2.0
 #mgv7_np_cave2 = 0, 12, (100, 100, 100), 10325, 4, 0.5, 2.0
 
+#    Prevent mods from doing insecure things like running shell commands.
+#secure.enable_security = false
+
index 45188f791a4bfffa284625fdb0e868d05257ed7c..f26b4c8ad5440e25a7040add97f236fc590a9d5e 100644 (file)
@@ -272,6 +272,7 @@ void set_default_settings(Settings *settings)
        settings->setDefault("emergequeue_limit_diskonly", "32");
        settings->setDefault("emergequeue_limit_generate", "32");
        settings->setDefault("num_emerge_threads", "1");
+       settings->setDefault("secure.enable_security", "false");
 
        // physics stuff
        settings->setDefault("movement_acceleration_default", "3");
index 4a4a2e4186b55f847edcbd3dda12dd89227a4852..9aeecf427e54b0dd7f0aaa66707bef5ee9b9c73e 100644 (file)
@@ -662,6 +662,19 @@ std::string RemoveRelativePathComponents(std::string path)
        return path.substr(0, pos);
 }
 
+std::string AbsolutePath(const std::string &path)
+{
+#ifdef _WIN32
+       char *abs_path = _fullpath(NULL, path.c_str(), MAX_PATH);
+#else
+       char *abs_path = realpath(path.c_str(), NULL);
+#endif
+       if (!abs_path) return "";
+       std::string abs_path_str(abs_path);
+       free(abs_path);
+       return abs_path_str;
+}
+
 const char *GetFilenameFromPath(const char *path)
 {
        const char *filename = strrchr(path, DIR_DELIM_CHAR);
index 7560d3c1524f3969af17e1c4c9e387cf29714d15..19fcbb6734a3dc5281ee7abdcf1ef4fba69ed348 100644 (file)
@@ -103,13 +103,17 @@ std::string RemoveLastPathComponent(const std::string &path,
 // this does not resolve symlinks and check for existence of directories.
 std::string RemoveRelativePathComponents(std::string path);
 
-// Return the filename from a path or the entire path if no directory delimiter
-// is found.
+// Returns the absolute path for the passed path, with "." and ".." path
+// components and symlinks removed.  Returns "" on error.
+std::string AbsolutePath(const std::string &path);
+
+// Returns the filename from a path or the entire path if no directory
+// delimiter is found.
 const char *GetFilenameFromPath(const char *path);
 
 bool safeWriteToFile(const std::string &path, const std::string &content);
 
-}//fs
+} // namespace fs
 
 #endif
 
index fcab98dc6c773fde1d212b96003e4b21c75e4e92..8cf39dc3ad6f642326654e5c375c980fee8e84af 100644 (file)
@@ -63,10 +63,8 @@ int script_exception_wrapper(lua_State *L, lua_CFunction f)
                return f(L);  // Call wrapped function and return result.
        } catch (const char *s) {  // Catch and convert exceptions.
                lua_pushstring(L, s);
-       } catch (std::exceptione) {
+       } catch (std::exception &e) {
                lua_pushstring(L, e.what());
-       } catch (...) {
-               lua_pushliteral(L, "caught (...)");
        }
        return lua_error(L);  // Rethrow as a Lua error.
 }
index 4584962f1d28dc161cb876e66c774f37ff316236..be4d0131e170089e95679259ed64438198339b3e 100644 (file)
@@ -1,4 +1,5 @@
 set(common_SCRIPT_CPP_API_SRCS
+       ${CMAKE_CURRENT_SOURCE_DIR}/s_async.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/s_base.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/s_entity.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/s_env.cpp
@@ -7,8 +8,8 @@ set(common_SCRIPT_CPP_API_SRCS
        ${CMAKE_CURRENT_SOURCE_DIR}/s_node.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/s_nodemeta.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/s_player.cpp
+       ${CMAKE_CURRENT_SOURCE_DIR}/s_security.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/s_server.cpp
-       ${CMAKE_CURRENT_SOURCE_DIR}/s_async.cpp
        PARENT_SCOPE)
 
 set(client_SCRIPT_CPP_API_SRCS
index 71473d2154028259b2c2c43941def463c8ed645c..4fb8411efe79d2abec05df7b8762d41ba3978b31 100644 (file)
@@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "cpp_api/s_base.h"
 #include "cpp_api/s_internal.h"
+#include "cpp_api/s_security.h"
 #include "lua_api/l_object.h"
 #include "serverobject.h"
 #include "debug.h"
@@ -45,18 +46,18 @@ class ModNameStorer
 private:
        lua_State *L;
 public:
-       ModNameStorer(lua_State *L_, const std::string &modname):
+       ModNameStorer(lua_State *L_, const std::string &mod_name):
                L(L_)
        {
-               // Store current modname in registry
-               lua_pushstring(L, modname.c_str());
-               lua_setfield(L, LUA_REGISTRYINDEX, "current_modname");
+               // Store current mod name in registry
+               lua_pushstring(L, mod_name.c_str());
+               lua_setfield(L, LUA_REGISTRYINDEX, SCRIPT_MOD_NAME_FIELD);
        }
        ~ModNameStorer()
        {
-               // Clear current modname in registry
+               // Clear current mod name from registry
                lua_pushnil(L);
-               lua_setfield(L, LUA_REGISTRYINDEX, "current_modname");
+               lua_setfield(L, LUA_REGISTRYINDEX, SCRIPT_MOD_NAME_FIELD);
        }
 };
 
@@ -112,32 +113,31 @@ ScriptApiBase::~ScriptApiBase()
        lua_close(m_luastack);
 }
 
-bool ScriptApiBase::loadMod(const std::string &scriptpath,
-               const std::string &modname)
+bool ScriptApiBase::loadMod(const std::string &script_path,
+               const std::string &mod_name)
 {
-       ModNameStorer modnamestorer(getStack(), modname);
+       ModNameStorer mod_name_storer(getStack(), mod_name);
 
-       if (!string_allowed(modname, MODNAME_ALLOWED_CHARS)) {
-               errorstream<<"Error loading mod \""<<modname
-                               <<"\": modname does not follow naming conventions: "
-                               <<"Only chararacters [a-z0-9_] are allowed."<<std::endl;
-               return false;
-       }
-
-       return loadScript(scriptpath);
+       return loadScript(script_path);
 }
 
-bool ScriptApiBase::loadScript(const std::string &scriptpath)
+bool ScriptApiBase::loadScript(const std::string &script_path)
 {
-       verbosestream<<"Loading and running script from "<<scriptpath<<std::endl;
+       verbosestream << "Loading and running script from " << script_path << std::endl;
 
        lua_State *L = getStack();
 
-       int ret = luaL_loadfile(L, scriptpath.c_str()) || lua_pcall(L, 0, 0, m_errorhandler);
-       if (ret) {
+       bool ok;
+       if (m_secure) {
+               ok = ScriptApiSecurity::safeLoadFile(L, script_path.c_str());
+       } else {
+               ok = !luaL_loadfile(L, script_path.c_str());
+       }
+       ok = ok && !lua_pcall(L, 0, 0, m_errorhandler);
+       if (!ok) {
                errorstream << "========== ERROR FROM LUA ===========" << std::endl;
                errorstream << "Failed to load and run script from " << std::endl;
-               errorstream << scriptpath << ":" << std::endl;
+               errorstream << script_path << ":" << std::endl;
                errorstream << std::endl;
                errorstream << lua_tostring(L, -1) << std::endl;
                errorstream << std::endl;
index 4ea3677a9a73b69d834fe214b2f3c890d5013fb3..cf9b7b934f9a30e94d35de61fc5aaa79691ad20d 100644 (file)
@@ -35,6 +35,12 @@ extern "C" {
 
 #define SCRIPTAPI_LOCK_DEBUG
 
+#define SCRIPT_MOD_NAME_FIELD "current_mod_name"
+// MUST be an invalid mod name so that mods can't
+// use that name to bypass security!
+#define BUILTIN_MOD_NAME "*builtin*"
+
+
 class Server;
 class Environment;
 class GUIEngine;
@@ -42,17 +48,18 @@ class ServerActiveObject;
 
 class ScriptApiBase {
 public:
-
        ScriptApiBase();
        virtual ~ScriptApiBase();
 
-       bool loadMod(const std::string &scriptpath, const std::string &modname);
-       bool loadScript(const std::string &scriptpath);
+       bool loadMod(const std::string &script_path, const std::string &mod_name);
+       bool loadScript(const std::string &script_path);
 
        /* object */
        void addObjectReference(ServerActiveObject *cobj);
        void removeObjectReference(ServerActiveObject *cobj);
 
+       Server* getServer() { return m_server; }
+
 protected:
        friend class LuaABM;
        friend class InvRef;
@@ -69,7 +76,6 @@ protected:
        void scriptError();
        void stackDump(std::ostream &o);
 
-       Server* getServer() { return m_server; }
        void setServer(Server* server) { m_server = server; }
 
        Environment* getEnv() { return m_environment; }
@@ -84,6 +90,7 @@ protected:
        JMutex          m_luastackmutex;
        // Stack index of Lua error handler
        int             m_errorhandler;
+       bool            m_secure;
 #ifdef SCRIPTAPI_LOCK_DEBUG
        bool            m_locked;
 #endif
diff --git a/src/script/cpp_api/s_security.cpp b/src/script/cpp_api/s_security.cpp
new file mode 100644 (file)
index 0000000..abe5b3e
--- /dev/null
@@ -0,0 +1,603 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "cpp_api/s_security.h"
+
+#include "filesys.h"
+#include "porting.h"
+#include "server.h"
+#include "settings.h"
+
+#include <cerrno>
+#include <string>
+#include <iostream>
+
+
+#define SECURE_API(lib, name) \
+       lua_pushcfunction(L, sl_##lib##_##name); \
+       lua_setfield(L, -2, #name);
+
+
+static inline void copy_safe(lua_State *L, const char *list[], unsigned len, int from=-2, int to=-1)
+{
+       if (from < 0) from = lua_gettop(L) + from + 1;
+       if (to   < 0) to   = lua_gettop(L) + to   + 1;
+       for (unsigned i = 0; i < (len / sizeof(list[0])); i++) {
+               lua_getfield(L, from, list[i]);
+               lua_setfield(L, to,   list[i]);
+       }
+}
+
+// Pushes the original version of a library function on the stack, from the old version
+static inline void push_original(lua_State *L, const char *lib, const char *func)
+{
+       lua_getfield(L, LUA_REGISTRYINDEX, "globals_backup");
+       lua_getfield(L, -1, lib);
+       lua_remove(L, -2);  // Remove globals_backup
+       lua_getfield(L, -1, func);
+       lua_remove(L, -2);  // Remove lib
+}
+
+
+void ScriptApiSecurity::initializeSecurity()
+{
+       static const char *whitelist[] = {
+               "assert",
+               "core",
+               "collectgarbage",
+               "DIR_DELIM",
+               "error",
+               "getfenv",
+               "getmetatable",
+               "ipairs",
+               "next",
+               "pairs",
+               "pcall",
+               "print",
+               "rawequal",
+               "rawget",
+               "rawset",
+               "select",
+               "setfenv",
+               "setmetatable",
+               "tonumber",
+               "tostring",
+               "type",
+               "unpack",
+               "_VERSION",
+               "xpcall",
+               // Completely safe libraries
+               "coroutine",
+               "string",
+               "table",
+               "math",
+       };
+       static const char *io_whitelist[] = {
+               "close",
+               "flush",
+               "read",
+               "type",
+               "write",
+       };
+       static const char *os_whitelist[] = {
+               "clock",
+               "date",
+               "difftime",
+               "exit",
+               "getenv",
+               "setlocale",
+               "time",
+               "tmpname",
+       };
+       static const char *debug_whitelist[] = {
+               "gethook",
+               "traceback",
+               "getinfo",
+               "getmetatable",
+               "setupvalue",
+               "setmetatable",
+               "upvalueid",
+               "upvaluejoin",
+               "sethook",
+               "debug",
+               "getupvalue",
+               "setlocal",
+       };
+       static const char *package_whitelist[] = {
+               "config",
+               "cpath",
+               "path",
+               "searchpath",
+       };
+       static const char *jit_whitelist[] = {
+               "arch",
+               "flush",
+               "off",
+               "on",
+               "opt",
+               "os",
+               "status",
+               "version",
+               "version_num",
+       };
+
+       m_secure = true;
+
+       lua_State *L = getStack();
+
+       // Backup globals to the registry
+       lua_getglobal(L, "_G");
+       lua_setfield(L, LUA_REGISTRYINDEX, "globals_backup");
+
+       // Replace the global environment with an empty one
+#if LUA_VERSION_NUM <= 501
+       int is_main = lua_pushthread(L);  // Push the main thread
+       FATAL_ERROR_IF(!is_main, "Security: ScriptApi's Lua state "
+                       "isn't the main Lua thread!");
+#endif
+       lua_newtable(L);  // Create new environment
+       lua_pushvalue(L, -1);
+       lua_setfield(L, -2, "_G");  // Set _G of new environment
+#if LUA_VERSION_NUM >= 502  // Lua >= 5.2
+       // Set the global environment
+       lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
+#else  // Lua <= 5.1
+       // Set the environment of the main thread
+       FATAL_ERROR_IF(!lua_setfenv(L, -2), "Security: Unable to set "
+                       "environment of the main Lua thread!");
+       lua_pop(L, 1);  // Pop thread
+#endif
+
+       // Get old globals
+       lua_getfield(L, LUA_REGISTRYINDEX, "globals_backup");
+       int old_globals = lua_gettop(L);
+
+
+       // Copy safe base functions
+       lua_getglobal(L, "_G");
+       copy_safe(L, whitelist, sizeof(whitelist));
+
+       // And replace unsafe ones
+       SECURE_API(g, dofile);
+       SECURE_API(g, load);
+       SECURE_API(g, loadfile);
+       SECURE_API(g, loadstring);
+       SECURE_API(g, require);
+       lua_pop(L, 1);
+
+
+       // Copy safe IO functions
+       lua_getfield(L, old_globals, "io");
+       lua_newtable(L);
+       copy_safe(L, io_whitelist, sizeof(io_whitelist));
+
+       // And replace unsafe ones
+       SECURE_API(io, open);
+       SECURE_API(io, input);
+       SECURE_API(io, output);
+       SECURE_API(io, lines);
+
+       lua_setglobal(L, "io");
+       lua_pop(L, 1);  // Pop old IO
+
+
+       // Copy safe OS functions
+       lua_getfield(L, old_globals, "os");
+       lua_newtable(L);
+       copy_safe(L, os_whitelist, sizeof(os_whitelist));
+
+       // And replace unsafe ones
+       SECURE_API(os, remove);
+       SECURE_API(os, rename);
+
+       lua_setglobal(L, "os");
+       lua_pop(L, 1);  // Pop old OS
+
+
+       // Copy safe debug functions
+       lua_getfield(L, old_globals, "debug");
+       lua_newtable(L);
+       copy_safe(L, debug_whitelist, sizeof(debug_whitelist));
+       lua_setglobal(L, "debug");
+       lua_pop(L, 1);  // Pop old debug
+
+
+       // Copy safe package fields
+       lua_getfield(L, old_globals, "package");
+       lua_newtable(L);
+       copy_safe(L, package_whitelist, sizeof(package_whitelist));
+       lua_setglobal(L, "package");
+       lua_pop(L, 1);  // Pop old package
+
+
+       // Copy safe jit functions, if they exist
+       lua_getfield(L, -1, "jit");
+       if (!lua_isnil(L, -1)) {
+               lua_newtable(L);
+               copy_safe(L, jit_whitelist, sizeof(jit_whitelist));
+               lua_setglobal(L, "jit");
+       }
+       lua_pop(L, 1);  // Pop old jit
+
+       lua_pop(L, 1); // Pop globals_backup
+}
+
+
+bool ScriptApiSecurity::isSecure(lua_State *L)
+{
+       lua_getfield(L, LUA_REGISTRYINDEX, "globals_backup");
+       bool secure = !lua_isnil(L, -1);
+       lua_pop(L, 1);
+       return secure;
+}
+
+
+#define CHECK_FILE_ERR(ret, fp) \
+       if (ret) { \
+               if (fp) std::fclose(fp); \
+               lua_pushfstring(L, "%s: %s", path, strerror(errno)); \
+               return false; \
+       }
+
+
+bool ScriptApiSecurity::safeLoadFile(lua_State *L, const char *path)
+{
+       FILE *fp;
+       char *chunk_name;
+       if (path == NULL) {
+               fp = stdin;
+               chunk_name = const_cast<char *>("=stdin");
+       } else {
+               fp = fopen(path, "r");
+               if (!fp) {
+                       lua_pushfstring(L, "%s: %s", path, strerror(errno));
+                       return false;
+               }
+               chunk_name = new char[strlen(path) + 2];
+               chunk_name[0] = '@';
+               chunk_name[1] = '\0';
+               strcat(chunk_name, path);
+       }
+
+       size_t start = 0;
+       int c = std::getc(fp);
+       if (c == '#') {
+               // Skip the first line
+               while ((c = std::getc(fp)) != EOF && c != '\n');
+               if (c == '\n') c = std::getc(fp);
+               start = std::ftell(fp);
+       }
+
+       if (c == LUA_SIGNATURE[0]) {
+               lua_pushliteral(L, "Bytecode prohibited when mod security is enabled.");
+               return false;
+       }
+
+       // Read the file
+       int ret = std::fseek(fp, 0, SEEK_END);
+       CHECK_FILE_ERR(ret, fp);
+       if (ret) {
+               std::fclose(fp);
+               lua_pushfstring(L, "%s: %s", path, strerror(errno));
+               return false;
+       }
+       size_t size = std::ftell(fp) - start;
+       char *code = new char[size];
+       ret = std::fseek(fp, start, SEEK_SET);
+       CHECK_FILE_ERR(ret, fp);
+       if (ret) {
+               std::fclose(fp);
+               lua_pushfstring(L, "%s: %s", path, strerror(errno));
+               return false;
+       }
+       size_t num_read = std::fread(code, 1, size, fp);
+       if (path) {
+               std::fclose(fp);
+       }
+       if (num_read != size) {
+               lua_pushliteral(L, "Error reading file to load.");
+               return false;
+       }
+
+       if (luaL_loadbuffer(L, code, size, chunk_name)) {
+               return false;
+       }
+
+       if (path) {
+               delete [] chunk_name;
+       }
+       return true;
+}
+
+
+bool ScriptApiSecurity::checkPath(lua_State *L, const char *path)
+{
+       std::string str;  // Transient
+
+       std::string norel_path = fs::RemoveRelativePathComponents(path);
+       std::string abs_path = fs::AbsolutePath(norel_path);
+
+       if (!abs_path.empty()) {
+               // Don't allow accessing the settings file
+               str = fs::AbsolutePath(g_settings_path);
+               if (str == abs_path) return false;
+       }
+
+       // If we couldn't find the absolute path (path doesn't exist) then
+       // try removing the last components until it works (to allow
+       // non-existent files/folders for mkdir).
+       std::string cur_path = norel_path;
+       std::string removed;
+       while (abs_path.empty() && !cur_path.empty()) {
+               std::string tmp_rmed;
+               cur_path = fs::RemoveLastPathComponent(cur_path, &tmp_rmed);
+               removed = tmp_rmed + (removed.empty() ? "" : DIR_DELIM + removed);
+               abs_path = fs::AbsolutePath(cur_path);
+       }
+       if (abs_path.empty()) return false;
+       // Add the removed parts back so that you can't, eg, create a
+       // directory in worldmods if worldmods doesn't exist.
+       if (!removed.empty()) abs_path += DIR_DELIM + removed;
+
+       // Get server from registry
+       lua_getfield(L, LUA_REGISTRYINDEX, "scriptapi");
+       ScriptApiBase *script = (ScriptApiBase *) lua_touserdata(L, -1);
+       lua_pop(L, 1);
+       const Server *server = script->getServer();
+
+       if (!server) return false;
+
+       // Get mod name
+       lua_getfield(L, LUA_REGISTRYINDEX, SCRIPT_MOD_NAME_FIELD);
+       if (lua_isstring(L, -1)) {
+               std::string mod_name = lua_tostring(L, -1);
+
+               // Builtin can access anything
+               if (mod_name == BUILTIN_MOD_NAME) {
+                       return true;
+               }
+
+               // Allow paths in mod path
+               const ModSpec *mod = server->getModSpec(mod_name);
+               if (mod) {
+                       str = fs::AbsolutePath(mod->path);
+                       if (!str.empty() && fs::PathStartsWith(abs_path, str)) {
+                               return true;
+                       }
+               }
+       }
+       lua_pop(L, 1);  // Pop mod name
+
+       str = fs::AbsolutePath(server->getWorldPath());
+       if (str.empty()) return false;
+       // Don't allow access to world mods.  We add to the absolute path
+       // of the world instead of getting the absolute paths directly
+       // because that won't work if they don't exist.
+       if (fs::PathStartsWith(abs_path, str + DIR_DELIM + "worldmods") ||
+                       fs::PathStartsWith(abs_path, str + DIR_DELIM + "game")) {
+               return false;
+       }
+       // Allow all other paths in world path
+       if (fs::PathStartsWith(abs_path, str)) {
+               return true;
+       }
+
+       // Default to disallowing
+       return false;
+}
+
+
+int ScriptApiSecurity::sl_g_dofile(lua_State *L)
+{
+       int nret = sl_g_loadfile(L);
+       if (nret != 1) {
+               return nret;
+       }
+       int top_precall = lua_gettop(L);
+       lua_call(L, 0, LUA_MULTRET);
+       // Return number of arguments returned by the function,
+       // adjusting for the function being poped.
+       return lua_gettop(L) - (top_precall - 1);
+}
+
+
+int ScriptApiSecurity::sl_g_load(lua_State *L)
+{
+       size_t len;
+       const char *buf;
+       std::string code;
+       const char *chunk_name = "=(load)";
+
+       luaL_checktype(L, 1, LUA_TFUNCTION);
+       if (!lua_isnone(L, 2)) {
+               luaL_checktype(L, 2, LUA_TSTRING);
+               chunk_name = lua_tostring(L, 2);
+       }
+
+       while (true) {
+               lua_pushvalue(L, 1);
+               lua_call(L, 0, 1);
+               int t = lua_type(L, -1);
+               if (t == LUA_TNIL) {
+                       break;
+               } else if (t != LUA_TSTRING) {
+                       lua_pushnil(L);
+                       lua_pushliteral(L, "Loader didn't return a string");
+                       return 2;
+               }
+               buf = lua_tolstring(L, -1, &len);
+               code += std::string(buf, len);
+               lua_pop(L, 1); // Pop return value
+       }
+       if (code[0] == LUA_SIGNATURE[0]) {
+               lua_pushnil(L);
+               lua_pushliteral(L, "Bytecode prohibited when mod security is enabled.");
+               return 2;
+       }
+       if (luaL_loadbuffer(L, code.data(), code.size(), chunk_name)) {
+               lua_pushnil(L);
+               lua_insert(L, lua_gettop(L) - 1);
+               return 2;
+       }
+       return 1;
+}
+
+
+int ScriptApiSecurity::sl_g_loadfile(lua_State *L)
+{
+       const char *path = NULL;
+
+       if (lua_isstring(L, 1)) {
+               path = lua_tostring(L, 1);
+               CHECK_SECURE_PATH(L, path);
+       }
+
+       if (!safeLoadFile(L, path)) {
+               lua_pushnil(L);
+               lua_insert(L, -2);
+               return 2;
+       }
+
+       return 1;
+}
+
+
+int ScriptApiSecurity::sl_g_loadstring(lua_State *L)
+{
+       const char *chunk_name = "=(load)";
+
+       luaL_checktype(L, 1, LUA_TSTRING);
+       if (!lua_isnone(L, 2)) {
+               luaL_checktype(L, 2, LUA_TSTRING);
+               chunk_name = lua_tostring(L, 2);
+       }
+
+       size_t size;
+       const char *code = lua_tolstring(L, 1, &size);
+
+       if (size > 0 && code[0] == LUA_SIGNATURE[0]) {
+               lua_pushnil(L);
+               lua_pushliteral(L, "Bytecode prohibited when mod security is enabled.");
+               return 2;
+       }
+       if (luaL_loadbuffer(L, code, size, chunk_name)) {
+               lua_pushnil(L);
+               lua_insert(L, lua_gettop(L) - 1);
+               return 2;
+       }
+       return 1;
+}
+
+
+int ScriptApiSecurity::sl_g_require(lua_State *L)
+{
+       lua_pushliteral(L, "require() is disabled when mod security is on.");
+       return lua_error(L);
+}
+
+
+int ScriptApiSecurity::sl_io_open(lua_State *L)
+{
+       luaL_checktype(L, 1, LUA_TSTRING);
+       const char *path = lua_tostring(L, 1);
+       CHECK_SECURE_PATH(L, path);
+
+       push_original(L, "io", "open");
+       lua_pushvalue(L, 1);
+       lua_pushvalue(L, 2);
+       lua_call(L, 2, 2);
+       return 2;
+}
+
+
+int ScriptApiSecurity::sl_io_input(lua_State *L)
+{
+       if (lua_isstring(L, 1)) {
+               const char *path = lua_tostring(L, 1);
+               CHECK_SECURE_PATH(L, path);
+       }
+
+       push_original(L, "io", "input");
+       lua_pushvalue(L, 1);
+       lua_call(L, 1, 1);
+       return 1;
+}
+
+
+int ScriptApiSecurity::sl_io_output(lua_State *L)
+{
+       if (lua_isstring(L, 1)) {
+               const char *path = lua_tostring(L, 1);
+               CHECK_SECURE_PATH(L, path);
+       }
+
+       push_original(L, "io", "output");
+       lua_pushvalue(L, 1);
+       lua_call(L, 1, 1);
+       return 1;
+}
+
+
+int ScriptApiSecurity::sl_io_lines(lua_State *L)
+{
+       if (lua_isstring(L, 1)) {
+               const char *path = lua_tostring(L, 1);
+               CHECK_SECURE_PATH(L, path);
+       }
+
+       push_original(L, "io", "lines");
+       lua_pushvalue(L, 1);
+       int top_precall = lua_gettop(L);
+       lua_call(L, 1, LUA_MULTRET);
+       // Return number of arguments returned by the function,
+       // adjusting for the function being poped.
+       return lua_gettop(L) - (top_precall - 1);
+}
+
+
+int ScriptApiSecurity::sl_os_rename(lua_State *L)
+{
+       luaL_checktype(L, 1, LUA_TSTRING);
+       const char *path1 = lua_tostring(L, 1);
+       CHECK_SECURE_PATH(L, path1);
+
+       luaL_checktype(L, 2, LUA_TSTRING);
+       const char *path2 = lua_tostring(L, 2);
+       CHECK_SECURE_PATH(L, path2);
+
+       push_original(L, "os", "rename");
+       lua_pushvalue(L, 1);
+       lua_pushvalue(L, 2);
+       lua_call(L, 2, 2);
+       return 2;
+}
+
+
+int ScriptApiSecurity::sl_os_remove(lua_State *L)
+{
+       luaL_checktype(L, 1, LUA_TSTRING);
+       const char *path = lua_tostring(L, 1);
+       CHECK_SECURE_PATH(L, path);
+
+       push_original(L, "os", "remove");
+       lua_pushvalue(L, 1);
+       lua_call(L, 1, 2);
+       return 2;
+}
+
diff --git a/src/script/cpp_api/s_security.h b/src/script/cpp_api/s_security.h
new file mode 100644 (file)
index 0000000..4a4389c
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+Minetest
+Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef S_SECURITY_H
+#define S_SECURITY_H
+
+#include "cpp_api/s_base.h"
+
+
+#define CHECK_SECURE_PATH(L, path) \
+       if (!ScriptApiSecurity::checkPath(L, path)) { \
+               lua_pushstring(L, (std::string("Attempt to access external file ") + \
+                                       path + " with mod security on.").c_str()); \
+               lua_error(L); \
+       }
+#define CHECK_SECURE_PATH_OPTIONAL(L, path) \
+       if (ScriptApiSecurity::isSecure(L)) { \
+               CHECK_SECURE_PATH(L, path); \
+       }
+
+
+class ScriptApiSecurity : virtual public ScriptApiBase
+{
+public:
+       // Sets up security on the ScriptApi's Lua state
+       void initializeSecurity();
+       // Checks if the Lua state has been secured
+       static bool isSecure(lua_State *L);
+       // Loads a file as Lua code safely (doesn't allow bytecode).
+       static bool safeLoadFile(lua_State *L, const char *path);
+       // Checks if mods are allowed to read and write to the path
+       static bool checkPath(lua_State *L, const char *path);
+
+private:
+       // Syntax: "sl_" <Library name or 'g' (global)> '_' <Function name>
+       // (sl stands for Secure Lua)
+
+       static int sl_g_dofile(lua_State *L);
+       static int sl_g_load(lua_State *L);
+       static int sl_g_loadfile(lua_State *L);
+       static int sl_g_loadstring(lua_State *L);
+       static int sl_g_require(lua_State *L);
+
+       static int sl_io_open(lua_State *L);
+       static int sl_io_input(lua_State *L);
+       static int sl_io_output(lua_State *L);
+       static int sl_io_lines(lua_State *L);
+
+       static int sl_os_rename(lua_State *L);
+       static int sl_os_remove(lua_State *L);
+};
+
+#endif
+
index d94f902c426684c9124fceaae5f1e5d467d962d7..dc3644e1cbe51de2d190ba0670e34193f63d4e03 100644 (file)
@@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_vmanip.h"
 #include "common/c_converter.h"
 #include "common/c_content.h"
+#include "cpp_api/s_security.h"
 #include "util/serialize.h"
 #include "server.h"
 #include "environment.h"
@@ -1031,6 +1032,10 @@ int ModApiMapgen::l_generate_decorations(lua_State *L)
 int ModApiMapgen::l_create_schematic(lua_State *L)
 {
        INodeDefManager *ndef = getServer(L)->getNodeDefManager();
+
+       const char *filename = luaL_checkstring(L, 4);
+       CHECK_SECURE_PATH_OPTIONAL(L, filename);
+
        Map *map = &(getEnv(L)->getMap());
        Schematic schem;
 
@@ -1069,8 +1074,6 @@ int ModApiMapgen::l_create_schematic(lua_State *L)
                }
        }
 
-       const char *filename = luaL_checkstring(L, 4);
-
        if (!schem.getSchematicFromMap(map, p1, p2)) {
                errorstream << "create_schematic: failed to get schematic "
                        "from map" << std::endl;
index 99e73b03e50b95f85aae434becbac6753f42853a..0d892631702d5509ef13f8670351c619f04f8fe6 100644 (file)
@@ -21,6 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "lua_api/l_internal.h"
 #include "common/c_converter.h"
 #include "common/c_content.h"
+#include "cpp_api/s_base.h"
 #include "server.h"
 #include "environment.h"
 #include "player.h"
@@ -342,7 +343,7 @@ int ModApiServer::l_show_formspec(lua_State *L)
 int ModApiServer::l_get_current_modname(lua_State *L)
 {
        NO_MAP_LOCK_REQUIRED;
-       lua_getfield(L, LUA_REGISTRYINDEX, "current_modname");
+       lua_getfield(L, LUA_REGISTRYINDEX, SCRIPT_MOD_NAME_FIELD);
        return 1;
 }
 
index 9c88a3e05e7c64dd1bdf94d5e0a9600e0e4d1b14..35b82b435cc677b9f47f8bd8a7316bc9668b4021 100644 (file)
@@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "lua_api/l_settings.h"
 #include "lua_api/l_internal.h"
+#include "cpp_api/s_security.h"
 #include "settings.h"
 #include "log.h"
 
@@ -188,6 +189,7 @@ int LuaSettings::create_object(lua_State* L)
 {
        NO_MAP_LOCK_REQUIRED;
        const char* filename = luaL_checkstring(L, 1);
+       CHECK_SECURE_PATH_OPTIONAL(L, filename);
        LuaSettings* o = new LuaSettings(filename);
        *(void **)(lua_newuserdata(L, sizeof(void *))) = o;
        luaL_getmetatable(L, className);
index 283cca01f9124f61f5b2cf5e17af18feebb41146..151d449d5476ee222723720985930434f18cb38f 100644 (file)
@@ -92,12 +92,19 @@ int ModApiUtil::l_log(lua_State *L)
        return 0;
 }
 
+#define CHECK_SECURE_SETTING(L, name) \
+       if (name.compare(0, 7, "secure.") == 0) {\
+               lua_pushliteral(L, "Attempt to set secure setting.");\
+               lua_error(L);\
+       }
+
 // setting_set(name, value)
 int ModApiUtil::l_setting_set(lua_State *L)
 {
        NO_MAP_LOCK_REQUIRED;
-       const char *name = luaL_checkstring(L, 1);
-       const char *value = luaL_checkstring(L, 2);
+       std::string name = luaL_checkstring(L, 1);
+       std::string value = luaL_checkstring(L, 2);
+       CHECK_SECURE_SETTING(L, name);
        g_settings->set(name, value);
        return 0;
 }
@@ -120,8 +127,9 @@ int ModApiUtil::l_setting_get(lua_State *L)
 int ModApiUtil::l_setting_setbool(lua_State *L)
 {
        NO_MAP_LOCK_REQUIRED;
-       const char *name = luaL_checkstring(L, 1);
+       std::string name = luaL_checkstring(L, 1);
        bool value = lua_toboolean(L, 2);
+       CHECK_SECURE_SETTING(L, name);
        g_settings->setBool(name, value);
        return 0;
 }
index 5bcd2a33df8693b047b1c16a19c48b195197b794..9321c38a90d04f70ddfddc28819fbed53711f195 100644 (file)
@@ -20,6 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "scripting_game.h"
 #include "server.h"
 #include "log.h"
+#include "settings.h"
 #include "cpp_api/s_internal.h"
 #include "lua_api/l_base.h"
 #include "lua_api/l_craft.h"
@@ -49,10 +50,12 @@ GameScripting::GameScripting(Server* server)
        // setEnv(env) is called by ScriptApiEnv::initializeEnvironment()
        // once the environment has been created
 
-       //TODO add security
-
        SCRIPTAPI_PRECHECKHEADER
 
+       if (g_settings->getBool("secure.enable_security")) {
+               initializeSecurity();
+       }
+
        lua_getglobal(L, "core");
        int top = lua_gettop(L);
 
index 14dbd9170d2df4fb98280dc8f2f1b8d987705077..16d5dcb37a6ee8d9229fbf946bee40b335796494 100644 (file)
@@ -27,19 +27,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "cpp_api/s_node.h"
 #include "cpp_api/s_player.h"
 #include "cpp_api/s_server.h"
+#include "cpp_api/s_security.h"
 
 /*****************************************************************************/
 /* Scripting <-> Game Interface                                              */
 /*****************************************************************************/
 
-class GameScripting
-               : virtual public ScriptApiBase,
-                 public ScriptApiDetached,
-                 public ScriptApiEntity,
-                 public ScriptApiEnv,
-                 public ScriptApiNode,
-                 public ScriptApiPlayer,
-                 public ScriptApiServer
+class GameScripting :
+               virtual public ScriptApiBase,
+               public ScriptApiDetached,
+               public ScriptApiEntity,
+               public ScriptApiEnv,
+               public ScriptApiNode,
+               public ScriptApiPlayer,
+               public ScriptApiServer,
+               public ScriptApiSecurity
 {
 public:
        GameScripting(Server* server);
index 54b3133c5064be1bda6fa8f23e9346cb7e0f7873..c74c18edc25570750c62334a3882065805936bdd 100644 (file)
@@ -38,8 +38,6 @@ MainMenuScripting::MainMenuScripting(GUIEngine* guiengine)
 {
        setGuiEngine(guiengine);
 
-       //TODO add security
-
        SCRIPTAPI_PRECHECKHEADER
 
        lua_getglobal(L, "core");
index f032da406de5bd1bdb37ad00f0849cf295b94052..778a932417f285b24edf3b80fe927068d1abba1e 100644 (file)
@@ -295,31 +295,37 @@ Server::Server(
 
        m_script = new GameScripting(this);
 
-       std::string scriptpath = getBuiltinLuaPath() + DIR_DELIM "init.lua";
+       std::string script_path = getBuiltinLuaPath() + DIR_DELIM "init.lua";
 
-       if (!m_script->loadScript(scriptpath))
-               throw ModError("Failed to load and run " + scriptpath);
+       if (!m_script->loadMod(script_path, BUILTIN_MOD_NAME)) {
+               throw ModError("Failed to load and run " + script_path);
+       }
 
-       // Print 'em
-       infostream<<"Server: Loading mods: ";
+       // Print mods
+       infostream << "Server: Loading mods: ";
        for(std::vector<ModSpec>::iterator i = m_mods.begin();
                        i != m_mods.end(); i++){
                const ModSpec &mod = *i;
-               infostream<<mod.name<<" ";
+               infostream << mod.name << " ";
        }
-       infostream<<std::endl;
+       infostream << std::endl;
        // Load and run "mod" scripts
-       for(std::vector<ModSpec>::iterator i = m_mods.begin();
-                       i != m_mods.end(); i++){
+       for (std::vector<ModSpec>::iterator i = m_mods.begin();
+                       i != m_mods.end(); i++) {
                const ModSpec &mod = *i;
-               std::string scriptpath = mod.path + DIR_DELIM + "init.lua";
-               infostream<<"  ["<<padStringRight(mod.name, 12)<<"] [\""
-                               <<scriptpath<<"\"]"<<std::endl;
-               bool success = m_script->loadMod(scriptpath, mod.name);
-               if(!success){
-                       errorstream<<"Server: Failed to load and run "
-                                       <<scriptpath<<std::endl;
-                       throw ModError("Failed to load and run "+scriptpath);
+               if (!string_allowed(mod.name, MODNAME_ALLOWED_CHARS)) {
+                       errorstream << "Error loading mod \"" << mod.name
+                                       << "\": mod_name does not follow naming conventions: "
+                                       << "Only chararacters [a-z0-9_] are allowed." << std::endl;
+                       throw ModError("Mod \"" + mod.name + "\" does not follow naming conventions.");
+               }
+               std::string script_path = mod.path + DIR_DELIM "init.lua";
+               infostream << "  [" << padStringRight(mod.name, 12) << "] [\""
+                               << script_path << "\"]" << std::endl;
+               if (!m_script->loadMod(script_path, mod.name)) {
+                       errorstream << "Server: Failed to load and run "
+                                       << script_path << std::endl;
+                       throw ModError("Failed to load and run " + script_path);
                }
        }
 
@@ -3206,9 +3212,9 @@ IWritableCraftDefManager* Server::getWritableCraftDefManager()
        return m_craftdef;
 }
 
-const ModSpec* Server::getModSpec(const std::string &modname)
+const ModSpec* Server::getModSpec(const std::string &modname) const
 {
-       for(std::vector<ModSpec>::iterator i = m_mods.begin();
+       for(std::vector<ModSpec>::const_iterator i = m_mods.begin();
                        i != m_mods.end(); i++){
                const ModSpec &mod = *i;
                if(mod.name == modname)
index f53e23a3acde5d3174dab431adee86127c2176e1..2030d6669840f061ab37d33eef146e9436ff8a0e 100644 (file)
@@ -322,10 +322,10 @@ public:
        IWritableNodeDefManager* getWritableNodeDefManager();
        IWritableCraftDefManager* getWritableCraftDefManager();
 
-       const ModSpec* getModSpec(const std::string &modname);
+       const ModSpec* getModSpec(const std::string &modname) const;
        void getModNames(std::vector<std::string> &modlist);
        std::string getBuiltinLuaPath();
-       inline std::string getWorldPath()
+       inline std::string getWorldPath() const
                        { return m_path_world; }
 
        inline bool isSingleplayer()
index 9adcd158785daf51f73c415c42b54f780f3db8b0..e95bd436d527dcda81df3d98186380880ca90712 100644 (file)
@@ -68,10 +68,11 @@ Settings & Settings::operator = (const Settings &other)
 
 bool Settings::checkNameValid(const std::string &name)
 {
-       size_t pos = name.find_first_of("\t\n\v\f\r\b =\"{}#");
-       if (pos != std::string::npos) {
-               errorstream << "Invalid character '" << name[pos]
-                       << "' found in setting name" << std::endl;
+       bool valid = name.find_first_of("=\"{}#") == std::string::npos;
+       if (valid) valid = trim(name) == name;
+       if (!valid) {
+               errorstream << "Invalid setting name \"" << name << "\""
+                       << std::endl;
                return false;
        }
        return true;
@@ -83,7 +84,7 @@ bool Settings::checkValueValid(const std::string &value)
        if (value.substr(0, 3) == "\"\"\"" ||
                value.find("\n\"\"\"") != std::string::npos) {
                errorstream << "Invalid character sequence '\"\"\"' found in"
-                       " setting value" << std::endl;
+                       " setting value!" << std::endl;
                return false;
        }
        return true;
@@ -92,9 +93,9 @@ bool Settings::checkValueValid(const std::string &value)
 
 std::string Settings::sanitizeName(const std::string &name)
 {
-       std::string n(name);
+       std::string n = trim(name);
 
-       for (const char *s = "\t\n\v\f\r\b =\"{}#"; *s; s++)
+       for (const char *s = "=\"{}#"; *s; s++)
                n.erase(std::remove(n.begin(), n.end(), *s), n.end());
 
        return n;