From 1fd4e0b82dd4e15aef0025eccdcb3bc984f70d9d Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sun, 3 Nov 2024 14:24:35 +0100 Subject: [PATCH] Refactor ScriptApiSecurity for cleaner separation of concerns --- src/script/common/c_internal.cpp | 10 +- src/script/common/c_internal.h | 1 + src/script/cpp_api/s_async.h | 8 +- src/script/cpp_api/s_base.cpp | 33 +----- src/script/cpp_api/s_base.h | 20 ++-- src/script/cpp_api/s_security.cpp | 183 +++++++++++++++++++----------- src/script/cpp_api/s_security.h | 68 +++++++++-- src/script/lua_api/l_util.cpp | 13 +-- src/script/scripting_client.h | 12 ++ src/script/scripting_emerge.h | 7 ++ src/script/scripting_server.h | 9 ++ 11 files changed, 229 insertions(+), 135 deletions(-) diff --git a/src/script/common/c_internal.cpp b/src/script/common/c_internal.cpp index ae6b5cc23..eb74bfa8e 100644 --- a/src/script/common/c_internal.cpp +++ b/src/script/common/c_internal.cpp @@ -3,6 +3,7 @@ // Copyright (C) 2013 celeron55, Perttu Ahola #include "common/c_internal.h" +#include "cpp_api/s_security.h" #include "util/numeric.h" #include "debug.h" #include "log.h" @@ -184,12 +185,9 @@ void log_deprecated(lua_State *L, std::string_view message, int stack_depth, boo void call_string_dump(lua_State *L, int idx) { - // Retrieve string.dump from insecure env to avoid it being tampered with - lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP); - if (!lua_isnil(L, -1)) - lua_getfield(L, -1, "string"); - else - lua_getglobal(L, "string"); + // Retrieve string.dump from untampered env + ScriptApiSecurity::getGlobalsBackup(L); + lua_getfield(L, -1, "string"); lua_getfield(L, -1, "dump"); lua_remove(L, -2); // remove _G lua_remove(L, -2); // remove 'string' table diff --git a/src/script/common/c_internal.h b/src/script/common/c_internal.h index e0b30044b..ce948801b 100644 --- a/src/script/common/c_internal.h +++ b/src/script/common/c_internal.h @@ -39,6 +39,7 @@ enum { #endif CUSTOM_RIDX_SCRIPTAPI, + /// @warning don't use directly, `ScriptApiSecurity` has wrappers CUSTOM_RIDX_GLOBALS_BACKUP, CUSTOM_RIDX_CURRENT_MOD_NAME, CUSTOM_RIDX_ERROR_HANDLER, diff --git a/src/script/cpp_api/s_async.h b/src/script/cpp_api/s_async.h index 3dc339821..8a081b2fc 100644 --- a/src/script/cpp_api/s_async.h +++ b/src/script/cpp_api/s_async.h @@ -50,11 +50,17 @@ class AsyncWorkerThread : public Thread, public: virtual ~AsyncWorkerThread(); - void *run(); + void *run() override; protected: AsyncWorkerThread(AsyncEngine* jobDispatcher, const std::string &name); + bool checkPathInternal(const std::string &abs_path, bool write_required, + bool *write_allowed) override { + return ScriptApiSecurity::checkPathWithGamedef(getStack(), + abs_path, write_required, write_allowed); + }; + private: AsyncEngine *jobDispatcher = nullptr; bool isErrored = false; diff --git a/src/script/cpp_api/s_base.cpp b/src/script/cpp_api/s_base.cpp index d04419302..9cf886efd 100644 --- a/src/script/cpp_api/s_base.cpp +++ b/src/script/cpp_api/s_base.cpp @@ -225,37 +225,6 @@ std::string ScriptApiBase::getCurrentModNameInsecure(lua_State *L) return ret; } -std::string ScriptApiBase::getCurrentModName(lua_State *L) -{ - auto script = ModApiBase::getScriptApiBase(L); - if (script->getType() == ScriptingType::Async || - script->getType() == ScriptingType::Emerge) - { - // As a precaution never return a "secure" mod name in the async and - // emerge environment, because these currently do not track mod origins - // in a spoof-safe way (see l_register_async_dofile and l_register_mapgen_script). - return ""; - } - - // We have to make sure that this function is being called directly by - // a mod, otherwise a malicious mod could override a function and - // steal its return value. (e.g. request_insecure_environment) - lua_Debug info; - - // Make sure there's only one item below this function on the stack... - if (lua_getstack(L, 2, &info)) - return ""; - FATAL_ERROR_IF(!lua_getstack(L, 1, &info), "lua_getstack() failed"); - FATAL_ERROR_IF(!lua_getinfo(L, "S", &info), "lua_getinfo() failed"); - - // ...and that that item is the main file scope. - if (strcmp(info.what, "main") != 0) - return ""; - - // at this point we can trust this value: - return getCurrentModNameInsecure(L); -} - void ScriptApiBase::loadMod(const std::string &script_path, const std::string &mod_name) { @@ -273,7 +242,7 @@ void ScriptApiBase::loadScript(const std::string &script_path) int error_handler = PUSH_ERROR_HANDLER(L); bool ok; - if (m_secure) { + if (ScriptApiSecurity::isSecure(L)) { ok = ScriptApiSecurity::safeLoadFile(L, script_path.c_str()); } else { ok = !luaL_loadfile(L, script_path.c_str()); diff --git a/src/script/cpp_api/s_base.h b/src/script/cpp_api/s_base.h index b08a887f0..34ac22595 100644 --- a/src/script/cpp_api/s_base.h +++ b/src/script/cpp_api/s_base.h @@ -101,17 +101,15 @@ public: void setOriginDirect(const char *origin); void setOriginFromTableRaw(int index, const char *fxn); - // Returns the currently running mod, only during init time. - // The reason this is "insecure" is that mods can mess with each others code, - // so the boundary of who is responsible is fuzzy. - // Note: checking this against BUILTIN_MOD_NAME is always safe (not spoofable). - // returns "" on error + /** + * Returns the currently running mod, only during init time. + * The reason this is insecure is that mods can mess with each others code, + * so the boundary of who is responsible is fuzzy. + * @note Checking this against BUILTIN_MOD_NAME is always safe (not spoofable). + * @note See ScriptApiSecurity::getCurrentModName() for the secure equivalent. + * @return mod name or "" on error + */ static std::string getCurrentModNameInsecure(lua_State *L); - // Returns the currently running mod, only during init time. - // This checks the Lua stack to only permit direct calls in the file - // scope. That way it is assured that it's really the mod it claims to be. - // returns "" on error - static std::string getCurrentModName(lua_State *L); #if !CHECK_CLIENT_BUILD() inline void clientOpenLibs(lua_State *L) { assert(false); } @@ -171,7 +169,7 @@ protected: std::recursive_mutex m_luastackmutex; std::string m_last_run_mod; - bool m_secure = false; + #ifdef SCRIPTAPI_LOCK_DEBUG int m_lock_recursion_count{}; std::thread::id m_owning_thread; diff --git a/src/script/cpp_api/s_security.cpp b/src/script/cpp_api/s_security.cpp index b23e94cd2..ef882a1f6 100644 --- a/src/script/cpp_api/s_security.cpp +++ b/src/script/cpp_api/s_security.cpp @@ -260,6 +260,8 @@ void ScriptApiSecurity::initializeSecurity() lua_pop(L, 1); // Pop empty string } +#if CHECK_CLIENT_BUILD() + void ScriptApiSecurity::initializeSecurityClient() { static const char *whitelist[] = { @@ -375,6 +377,8 @@ void ScriptApiSecurity::initializeSecurityClient() setLuaEnv(L, thread); } +#endif + int ScriptApiSecurity::getThread(lua_State *L) { #if LUA_VERSION_NUM <= 501 @@ -408,19 +412,24 @@ void ScriptApiSecurity::setLuaEnv(lua_State *L, int thread) bool ScriptApiSecurity::isSecure(lua_State *L) { -#if CHECK_CLIENT_BUILD() - auto script = ModApiBase::getScriptApiBase(L); - // CSM keeps no globals backup but is always secure - if (script->getType() == ScriptingType::Client) - return true; -#endif - lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP); - bool secure = !lua_isnil(L, -1); - lua_pop(L, 1); - return secure; + auto *script = ModApiBase::getScriptApiBase(L); + if (auto *sec = dynamic_cast(script)) + return sec->m_secure; + return false; } -bool ScriptApiSecurity::safeLoadString(lua_State *L, const std::string &code, const char *chunk_name) +void ScriptApiSecurity::getGlobalsBackup(lua_State *L) +{ + if (!ScriptApiSecurity::isSecure(L)) { + lua_getglobal(L, "_G"); + return; + } + lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP); + // We cannot fulfill the callers wish securely if they don't exist. + FATAL_ERROR_IF(lua_isnil(L, -1), "Globals backup requested, but it is not available. Cannot proceed securely."); +} + +bool ScriptApiSecurity::safeLoadString(lua_State *L, std::string_view code, const char *chunk_name) { if (code.size() > 0 && code[0] == LUA_SIGNATURE[0]) { lua_pushliteral(L, "Bytecode prohibited when mod security is enabled."); @@ -441,7 +450,7 @@ bool ScriptApiSecurity::safeLoadFile(lua_State *L, const char *path, const char fp = stdin; chunk_name = const_cast("=stdin"); } else { - fp = fopen(path, "rb"); + fp = std::fopen(path, "rb"); if (!fp) { lua_pushfstring(L, "%s: %s", path, strerror(errno)); return false; @@ -500,7 +509,35 @@ bool ScriptApiSecurity::safeLoadFile(lua_State *L, const char *path, const char } -bool checkModNameWhitelisted(const std::string &mod_name, const std::string &setting) +std::string ScriptApiSecurity::getCurrentModName(lua_State *L) +{ + auto *script = ModApiBase::getScriptApiBase(L); + + auto *sec = dynamic_cast(script); + if (sec && !sec->modNamesAreTrusted()) + return ""; + + // We have to make sure that this function is being called directly by + // a mod, otherwise a malicious mod could override a function and + // steal its return value. (e.g. request_insecure_environment) + lua_Debug info; + + // Make sure there's only one item below this function on the stack... + if (lua_getstack(L, 2, &info)) + return ""; + FATAL_ERROR_IF(!lua_getstack(L, 1, &info), "lua_getstack() failed"); + FATAL_ERROR_IF(!lua_getinfo(L, "S", &info), "lua_getinfo() failed"); + + // ...and that that item is the main file scope. + if (strcmp(info.what, "main") != 0) + return ""; + + // at this point we can trust this value: + return getCurrentModNameInsecure(L); +} + + +static bool checkModNameWhitelisted(const std::string &mod_name, const std::string &setting) { assert(str_starts_with(setting, "secure.")); @@ -517,7 +554,7 @@ bool checkModNameWhitelisted(const std::string &mod_name, const std::string &set bool ScriptApiSecurity::checkWhitelisted(lua_State *L, const std::string &setting) { - std::string mod_name = ScriptApiBase::getCurrentModName(L); + std::string mod_name = getCurrentModName(L); return checkModNameWhitelisted(mod_name, setting); } @@ -528,16 +565,8 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path, if (write_allowed) *write_allowed = false; - std::string str; // Transient - std::string abs_path = fs::AbsolutePath(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). @@ -560,61 +589,84 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path, } if (abs_path.empty()) return false; - // Add the removed parts back so that you can't, eg, create a + // Add the removed parts back so that you can e.g. create a // directory in worldmods if worldmods doesn't exist. if (!removed.empty()) abs_path += DIR_DELIM + removed; - // Get gamedef from registry - ScriptApiBase *script = ModApiBase::getScriptApiBase(L); - const IGameDef *gamedef = script->getGameDef(); + tracestream << "ScriptApiSecurity: path \"" << path << "\" resolved to \"" + << abs_path << "\"" << std::endl; + + // Ask the environment-specific implementation + auto *sec = ModApiBase::getScriptApi(L); + return sec->checkPathInternal(abs_path, write_required, write_allowed); +} + + +bool ScriptApiSecurity::checkPathWithGamedef(lua_State *L, + const std::string &abs_path, bool write_required, bool *write_allowed) +{ + const auto &set_write_allowed = [&] (bool v) { + if (write_allowed) + *write_allowed = v; + }; + std::string str; // Transient + + auto *gamedef = ModApiBase::getGameDef(L); if (!gamedef) return false; + if (!abs_path.empty()) { + // Don't allow accessing the settings file + str = fs::AbsolutePath(g_settings_path); + if (str == abs_path) + return false; + } + // Get mod name std::string mod_name = ScriptApiBase::getCurrentModNameInsecure(L); if (!mod_name.empty()) { // Builtin can access anything if (mod_name == BUILTIN_MOD_NAME) { - if (write_allowed) *write_allowed = true; + set_write_allowed(true); return true; } + } - // Allow paths in mod path - // Don't bother if write access isn't important, since it will be handled later - if (write_required || write_allowed != NULL) { - const ModSpec *mod = gamedef->getModSpec(mod_name); - if (mod) { - str = fs::AbsolutePath(mod->path); - if (!str.empty() && fs::PathStartsWith(abs_path, str)) { - // `mod_name` cannot be trusted here, so we catch the scenarios where this becomes a problem: - bool is_trusted = checkModNameWhitelisted(mod_name, "secure.trusted_mods") || - checkModNameWhitelisted(mod_name, "secure.http_mods"); - std::string filename = lowercase(fs::GetFilenameFromPath(abs_path.c_str())); - // By writing to any of these a malicious mod could turn itself into - // an existing trusted mod by renaming or becoming a modpack. - bool is_dangerous_file = filename == "mod.conf" || - filename == "modpack.conf" || - filename == "modpack.txt"; - if (write_required) { - if (is_trusted) { - throw LuaError( - "Unable to write to a trusted or http mod's directory. " - "For data storage consider minetest.get_mod_data_path() or minetest.get_worldpath() instead."); - } else if (is_dangerous_file) { - throw LuaError( - "Unable to write to special file for security reasons"); - } else { - const char *message = - "Writing to mod directories is deprecated, as any changes " - "will be overwritten when updating content. " - "For data storage consider minetest.get_mod_data_path() or minetest.get_worldpath() instead."; - log_deprecated(L, message, 1); - } + // Allow paths in mod path + // Don't bother if write access isn't important, since it will be handled later + if (write_required || write_allowed) { + const ModSpec *mod = gamedef->getModSpec(mod_name); + if (mod) { + str = fs::AbsolutePath(mod->path); + if (!str.empty() && fs::PathStartsWith(abs_path, str)) { + // `mod_name` cannot be trusted here, so we catch the scenarios where this becomes a problem: + bool is_trusted = checkModNameWhitelisted(mod_name, "secure.trusted_mods") || + checkModNameWhitelisted(mod_name, "secure.http_mods"); + std::string filename = lowercase(fs::GetFilenameFromPath(abs_path.c_str())); + // By writing to any of these a malicious mod could turn itself into + // an existing trusted mod by renaming or becoming a modpack. + bool is_dangerous_file = filename == "mod.conf" || + filename == "modpack.conf" || + filename == "modpack.txt"; + if (write_required) { + if (is_trusted) { + throw LuaError( + "Unable to write to a trusted or http mod's directory. " + "For data storage consider minetest.get_mod_data_path() or minetest.get_worldpath() instead."); + } else if (is_dangerous_file) { + throw LuaError( + "Unable to write to special file for security reasons"); + } else { + const char *message = + "Writing to mod directories is deprecated, as any changes " + "will be overwritten when updating content. " + "For data storage consider minetest.get_mod_data_path() or minetest.get_worldpath() instead."; + log_deprecated(L, message, 1); } - if (write_allowed) *write_allowed = !is_trusted && !is_dangerous_file; - return true; } + set_write_allowed(!is_trusted && !is_dangerous_file); + return true; } } } @@ -624,9 +676,8 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path, const SubgameSpec *game_spec = gamedef->getGameSpec(); if (game_spec && !game_spec->path.empty()) { str = fs::AbsolutePath(game_spec->path); - if (!str.empty() && fs::PathStartsWith(abs_path, str)) { + if (!str.empty() && fs::PathStartsWith(abs_path, str)) return true; - } } } @@ -635,16 +686,15 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path, const std::vector &mods = gamedef->getMods(); for (const ModSpec &mod : mods) { str = fs::AbsolutePath(mod.path); - if (!str.empty() && fs::PathStartsWith(abs_path, str)) { + if (!str.empty() && fs::PathStartsWith(abs_path, str)) return true; - } } } // Allow read/write access to all mod common dirs str = fs::AbsolutePath(gamedef->getModDataPath()); if (!str.empty() && fs::PathStartsWith(abs_path, str)) { - if (write_allowed) *write_allowed = true; + set_write_allowed(true); return true; } @@ -662,12 +712,11 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path, } // Allow all other paths in world path if (fs::PathStartsWith(abs_path, str)) { - if (write_allowed) *write_allowed = true; + set_write_allowed(true); return true; } } - // Default to disallowing return false; } diff --git a/src/script/cpp_api/s_security.h b/src/script/cpp_api/s_security.h index 91d5dce54..1241b0b06 100644 --- a/src/script/cpp_api/s_security.h +++ b/src/script/cpp_api/s_security.h @@ -12,10 +12,12 @@ throw LuaError(std::string("Mod security: Blocked attempted ") + \ (write_required ? "write to " : "read from ") + path); \ } + #define CHECK_SECURE_PATH(L, path, write_required) \ if (ScriptApiSecurity::isSecure(L)) { \ - CHECK_SECURE_PATH_INTERNAL(L, path, write_required, NULL); \ + CHECK_SECURE_PATH_INTERNAL(L, path, write_required, nullptr); \ } + #define CHECK_SECURE_PATH_POSSIBLE_WRITE(L, path, ptr) \ if (ScriptApiSecurity::isSecure(L)) { \ CHECK_SECURE_PATH_INTERNAL(L, path, false, ptr); \ @@ -27,19 +29,65 @@ class ScriptApiSecurity : virtual public ScriptApiBase public: // Sets up security on the ScriptApi's Lua state void initializeSecurity(); +#if CHECK_CLIENT_BUILD() void initializeSecurityClient(); +#else + inline void initializeSecurityClient() { assert(0); } +#endif + // Checks if the Lua state has been secured static bool isSecure(lua_State *L); - // Loads a string as Lua code safely (doesn't allow bytecode). - static bool safeLoadString(lua_State *L, const std::string &code, const char *chunk_name); - // Loads a file as Lua code safely (doesn't allow bytecode). - static bool safeLoadFile(lua_State *L, const char *path, const char *display_name = NULL); - // Check if mod is whitelisted in the given setting - // This additionally checks that the mod's main file scope is executing. + // Leaves the untampered globals (table) on top of the stack + static void getGlobalsBackup(lua_State *L); + + /// Loads a string as Lua code safely (doesn't allow bytecode). + static bool safeLoadString(lua_State *L, std::string_view code, const char *chunk_name); + /// Loads a file as Lua code safely (doesn't allow bytecode). + /// @warning path is not validated in any way + static bool safeLoadFile(lua_State *L, const char *path, const char *display_name = nullptr); + + /** + * Returns the currently running mod, only during init time. + * This checks the Lua stack to only permit direct calls in the file + * scope. That way it is assured that it's really the mod it claims to be. + * @return mod name or "" on error + */ + static std::string getCurrentModName(lua_State *L); + /// Check if mod is whitelisted in the given setting. + /// This additionally does main scope checks (see above method). + /// @note check is performed even in non-secured Lua state static bool checkWhitelisted(lua_State *L, const std::string &setting); - // Checks if mods are allowed to read (and optionally write) to the path + + /// Checks if mods are allowed to read (and optionally write) to the path + /// @note invalid to call in non-secured Lua state static bool checkPath(lua_State *L, const char *path, bool write_required, - bool *write_allowed=NULL); + bool *write_allowed = nullptr); + +protected: + // To be implemented by descendants: + + /** + * Specify if the mod names during init time(!) can be trusted. + * It needs to be assured that no tampering happens before any call to `loadMod()`. + * @note disabling this implies that mod whitelisting never works + * @return boolean value + */ + virtual bool modNamesAreTrusted() { return false; } + + /** + * Should check if the given path may be accessed. + * If `write_required` is true test for write access, if false test for read access. + * @param abs_path absolute file/directory path, may not exist + * @param write_required was write access requested? + * @param write_allowed output parameter (nullable): set to true if writing is allowed + * @return true if access is allowed + */ + virtual bool checkPathInternal(const std::string &abs_path, bool write_required, + bool *write_allowed) = 0; + + // Ready-made implementation of `checkPathInternal` suitable for server-related uses + static bool checkPathWithGamedef(lua_State *L, const std::string &abs_path, + bool write_required, bool *write_allowed); private: int getThread(lua_State *L); @@ -48,6 +96,8 @@ private: // creates an empty Lua environment void createEmptyEnv(lua_State *L); + bool m_secure = false; + // Syntax: "sl_" '_' // (sl stands for Secure Lua) diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp index e464cb3a2..cfea974b3 100644 --- a/src/script/lua_api/l_util.cpp +++ b/src/script/lua_api/l_util.cpp @@ -494,18 +494,13 @@ int ModApiUtil::l_request_insecure_environment(lua_State *L) { NO_MAP_LOCK_REQUIRED; - // Just return _G if security is disabled - if (!ScriptApiSecurity::isSecure(L)) { - lua_getglobal(L, "_G"); - return 1; - } - - if (!ScriptApiSecurity::checkWhitelisted(L, "secure.trusted_mods")) { - return 0; + if (ScriptApiSecurity::isSecure(L)) { + if (!ScriptApiSecurity::checkWhitelisted(L, "secure.trusted_mods")) + return 0; } // Push insecure environment - lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP); + ScriptApiSecurity::getGlobalsBackup(L); return 1; } diff --git a/src/script/scripting_client.h b/src/script/scripting_client.h index cfe4133c7..872ad295a 100644 --- a/src/script/scripting_client.h +++ b/src/script/scripting_client.h @@ -5,6 +5,8 @@ #pragma once +#include + #include "cpp_api/s_base.h" #include "cpp_api/s_client.h" #include "cpp_api/s_modchannels.h" @@ -27,6 +29,16 @@ public: void on_camera_ready(Camera *camera); void on_minimap_ready(Minimap *minimap); +protected: + // from ScriptApiSecurity: + bool checkPathInternal(const std::string &abs_path, bool write_required, + bool *write_allowed) override { + warningstream << "IO API called in client scripting" << std::endl; + assert(0); + return false; + } + bool modNamesAreTrusted() override { return true; } + private: virtual void InitializeModApi(lua_State *L, int top); }; diff --git a/src/script/scripting_emerge.h b/src/script/scripting_emerge.h index 8f3c7c348..8e645fd9d 100644 --- a/src/script/scripting_emerge.h +++ b/src/script/scripting_emerge.h @@ -17,6 +17,13 @@ class EmergeScripting: public: EmergeScripting(EmergeThread *parent); +protected: + bool checkPathInternal(const std::string &abs_path, bool write_required, + bool *write_allowed) override { + return ScriptApiSecurity::checkPathWithGamedef(getStack(), + abs_path, write_required, write_allowed); + }; + private: void InitializeModApi(lua_State *L, int top); }; diff --git a/src/script/scripting_server.h b/src/script/scripting_server.h index 54093e84a..6c0583553 100644 --- a/src/script/scripting_server.h +++ b/src/script/scripting_server.h @@ -50,6 +50,15 @@ public: u32 queueAsync(std::string &&serialized_func, PackedValue *param, const std::string &mod_origin); +protected: + // from ScriptApiSecurity: + bool checkPathInternal(const std::string &abs_path, bool write_required, + bool *write_allowed) override { + return ScriptApiSecurity::checkPathWithGamedef(getStack(), + abs_path, write_required, write_allowed); + } + bool modNamesAreTrusted() override { return true; } + private: void InitializeModApi(lua_State *L, int top);