diff options
author | ShadowNinja <shadowninja@minetest.net> | 2014-09-05 20:08:51 -0400 |
---|---|---|
committer | ShadowNinja <shadowninja@minetest.net> | 2015-05-16 18:32:31 -0400 |
commit | 3a8c7888807e4483bbdb3edd81c9893f3e2f427d (patch) | |
tree | 81f339e5f61b03e8d7842e06f034d09bf59dba96 | |
parent | f26421228bbd31f02bf16b45a4b82be84f233e52 (diff) | |
download | minetest-3a8c7888807e4483bbdb3edd81c9893f3e2f427d.tar.gz minetest-3a8c7888807e4483bbdb3edd81c9893f3e2f427d.tar.bz2 minetest-3a8c7888807e4483bbdb3edd81c9893f3e2f427d.zip |
Add mod security
Due to compatibility concerns, this is temporarily disabled.
-rw-r--r-- | build/android/jni/Android.mk | 3 | ||||
-rw-r--r-- | doc/lua_api.txt | 4 | ||||
-rw-r--r-- | minetest.conf.example | 3 | ||||
-rw-r--r-- | src/defaultsettings.cpp | 1 | ||||
-rw-r--r-- | src/filesys.cpp | 13 | ||||
-rw-r--r-- | src/filesys.h | 10 | ||||
-rw-r--r-- | src/script/common/c_internal.cpp | 4 | ||||
-rw-r--r-- | src/script/cpp_api/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/script/cpp_api/s_base.cpp | 44 | ||||
-rw-r--r-- | src/script/cpp_api/s_base.h | 15 | ||||
-rw-r--r-- | src/script/cpp_api/s_security.cpp | 603 | ||||
-rw-r--r-- | src/script/cpp_api/s_security.h | 70 | ||||
-rw-r--r-- | src/script/lua_api/l_mapgen.cpp | 7 | ||||
-rw-r--r-- | src/script/lua_api/l_server.cpp | 3 | ||||
-rw-r--r-- | src/script/lua_api/l_settings.cpp | 2 | ||||
-rw-r--r-- | src/script/lua_api/l_util.cpp | 14 | ||||
-rw-r--r-- | src/script/scripting_game.cpp | 7 | ||||
-rw-r--r-- | src/script/scripting_game.h | 18 | ||||
-rw-r--r-- | src/script/scripting_mainmenu.cpp | 2 | ||||
-rw-r--r-- | src/server.cpp | 44 | ||||
-rw-r--r-- | src/server.h | 4 | ||||
-rw-r--r-- | src/settings.cpp | 15 |
22 files changed, 809 insertions, 80 deletions
diff --git a/build/android/jni/Android.mk b/build/android/jni/Android.mk index 206c30ccf..f78b78b9b 100644 --- a/build/android/jni/Android.mk +++ b/build/android/jni/Android.mk @@ -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 \ diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 0cc83bf69..2421af069 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -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 diff --git a/minetest.conf.example b/minetest.conf.example index 4e3e97b95..6474289bd 100644 --- a/minetest.conf.example +++ b/minetest.conf.example @@ -569,3 +569,6 @@ #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 + diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 45188f791..f26b4c8ad 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -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"); diff --git a/src/filesys.cpp b/src/filesys.cpp index 4a4a2e418..9aeecf427 100644 --- a/src/filesys.cpp +++ b/src/filesys.cpp @@ -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); diff --git a/src/filesys.h b/src/filesys.h index 7560d3c15..19fcbb673 100644 --- a/src/filesys.h +++ b/src/filesys.h @@ -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 diff --git a/src/script/common/c_internal.cpp b/src/script/common/c_internal.cpp index fcab98dc6..8cf39dc3a 100644 --- a/src/script/common/c_internal.cpp +++ b/src/script/common/c_internal.cpp @@ -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::exception& e) { + } catch (std::exception &e) { lua_pushstring(L, e.what()); - } catch (...) { - lua_pushliteral(L, "caught (...)"); } return lua_error(L); // Rethrow as a Lua error. } diff --git a/src/script/cpp_api/CMakeLists.txt b/src/script/cpp_api/CMakeLists.txt index 4584962f1..be4d0131e 100644 --- a/src/script/cpp_api/CMakeLists.txt +++ b/src/script/cpp_api/CMakeLists.txt @@ -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 diff --git a/src/script/cpp_api/s_base.cpp b/src/script/cpp_api/s_base.cpp index 71473d215..4fb8411ef 100644 --- a/src/script/cpp_api/s_base.cpp +++ b/src/script/cpp_api/s_base.cpp @@ -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; diff --git a/src/script/cpp_api/s_base.h b/src/script/cpp_api/s_base.h index 4ea3677a9..cf9b7b934 100644 --- a/src/script/cpp_api/s_base.h +++ b/src/script/cpp_api/s_base.h @@ -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 index 000000000..abe5b3e97 --- /dev/null +++ b/src/script/cpp_api/s_security.cpp @@ -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 index 000000000..4a4389cf5 --- /dev/null +++ b/src/script/cpp_api/s_security.h @@ -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 + diff --git a/src/script/lua_api/l_mapgen.cpp b/src/script/lua_api/l_mapgen.cpp index d94f902c4..dc3644e1c 100644 --- a/src/script/lua_api/l_mapgen.cpp +++ b/src/script/lua_api/l_mapgen.cpp @@ -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; diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp index 99e73b03e..0d8926317 100644 --- a/src/script/lua_api/l_server.cpp +++ b/src/script/lua_api/l_server.cpp @@ -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; } diff --git a/src/script/lua_api/l_settings.cpp b/src/script/lua_api/l_settings.cpp index 9c88a3e05..35b82b435 100644 --- a/src/script/lua_api/l_settings.cpp +++ b/src/script/lua_api/l_settings.cpp @@ -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); diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp index 283cca01f..151d449d5 100644 --- a/src/script/lua_api/l_util.cpp +++ b/src/script/lua_api/l_util.cpp @@ -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; } diff --git a/src/script/scripting_game.cpp b/src/script/scripting_game.cpp index 5bcd2a33d..9321c38a9 100644 --- a/src/script/scripting_game.cpp +++ b/src/script/scripting_game.cpp @@ -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); diff --git a/src/script/scripting_game.h b/src/script/scripting_game.h index 14dbd9170..16d5dcb37 100644 --- a/src/script/scripting_game.h +++ b/src/script/scripting_game.h @@ -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); diff --git a/src/script/scripting_mainmenu.cpp b/src/script/scripting_mainmenu.cpp index 54b3133c5..c74c18edc 100644 --- a/src/script/scripting_mainmenu.cpp +++ b/src/script/scripting_mainmenu.cpp @@ -38,8 +38,6 @@ MainMenuScripting::MainMenuScripting(GUIEngine* guiengine) { setGuiEngine(guiengine); - //TODO add security - SCRIPTAPI_PRECHECKHEADER lua_getglobal(L, "core"); diff --git a/src/server.cpp b/src/server.cpp index f032da406..778a93241 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -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) diff --git a/src/server.h b/src/server.h index f53e23a3a..2030d6669 100644 --- a/src/server.h +++ b/src/server.h @@ -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() diff --git a/src/settings.cpp b/src/settings.cpp index 9adcd1587..e95bd436d 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -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; |