From f41a378e21e5dbed03b8127af7db2a746de8688d Mon Sep 17 00:00:00 2001 From: SAMURAI <66764345+xesdoog@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:09:23 +0100 Subject: [PATCH] refactor(LuaModule): isolate configs per module (#199) * refactor(LuaModule): isolate configs per module - Give each Lua module its own config folder. - Add sandboxed `os.rename` limited to the module's config folder. This allows atomic writes, useful for large config files that may get corrupted due to interruptions mid-write. * fix(os.rename): add documentation and properly handle errors * fix(lua_module): fix make_absolute - Reject absolute user paths. - Canonicalize root path. - Fix `os.rename` docs. - lua_manager: validate module names and skip loading modules with names that contain illegal characters. --- src/lua/lua_manager.cpp | 38 ++++++++++++++++- src/lua/lua_module.cpp | 91 +++++++++++++++++++++++++++++++---------- src/lua/lua_module.hpp | 3 +- 3 files changed, 108 insertions(+), 24 deletions(-) diff --git a/src/lua/lua_manager.cpp b/src/lua/lua_manager.cpp index 52b21161..59258d22 100644 --- a/src/lua/lua_manager.cpp +++ b/src/lua/lua_manager.cpp @@ -1,4 +1,4 @@ -#include "lua_manager.hpp" +#include "lua_manager.hpp" #include "file_manager.hpp" @@ -308,6 +308,34 @@ namespace big }); } + static inline bool validate_module_name(const std::string& name) + { + if (name.empty()) + { + return false; + } + + if (name == "." || name == "..") + { + return false; + } + + if (name.find("/") != std::string::npos || name.find("\\") != std::string::npos || name.find(":") != std::string::npos || name.find("..") != std::string::npos) + { + return false; + } + + for (unsigned char c : name) + { + if (std::iscntrl(c)) + { + return false; + } + } + + return true; + } + std::weak_ptr lua_manager::load_module(const std::filesystem::path& module_path) { if (!std::filesystem::exists(module_path)) @@ -321,7 +349,13 @@ namespace big return {}; const auto module_name = module_path.filename().string(); - const auto id = rage::joaat(module_name); + if (!validate_module_name(module_name)) + { + LOG(WARNING) << "Module " << module_name << " was not loaded. File name contains illegal characters."; + return {}; + } + + const auto id = rage::joaat(module_name); std::lock_guard guard(m_module_lock); for (const auto& module : m_modules) diff --git a/src/lua/lua_module.cpp b/src/lua/lua_module.cpp index 562c7825..df56f801 100644 --- a/src/lua/lua_module.cpp +++ b/src/lua/lua_module.cpp @@ -1,4 +1,4 @@ -#include "lua_module.hpp" +#include "lua_module.hpp" #include "bindings/command.hpp" #include "bindings/entities.hpp" @@ -84,11 +84,11 @@ namespace big } lua_module::lua_module(const std::filesystem::path& module_path, folder& scripts_folder, bool disabled) : - m_state(), - m_module_path(module_path), - m_module_name(module_path.filename().string()), - m_module_id(rage::joaat(m_module_name)), - m_disabled(disabled) + m_state(), + m_module_path(module_path), + m_module_name(module_path.filename().string()), + m_module_id(rage::joaat(m_module_name)), + m_disabled(disabled) { if (!m_disabled) { @@ -163,6 +163,15 @@ namespace big return m_disabled; } + const std::filesystem::path lua_module::get_config_folder() const + { + const auto config_path = g_lua_manager->get_scripts_config_folder().get_path() / m_module_name; + if (!std::filesystem::exists(config_path)) + std::filesystem::create_directory(config_path); + + return config_path; + } + void lua_module::set_folder_for_lua_require(folder& scripts_folder) { std::string scripts_search_path = scripts_folder.get_path().string() + "/?.lua;"; @@ -172,6 +181,9 @@ namespace big if (!entry.is_directory()) continue; + if (std::filesystem::relative(entry, scripts_folder.get_path()).wstring().contains(L"disabled")) + continue; + scripts_search_path += entry.path().string() + "/?.lua;"; } // Remove final ';' @@ -180,6 +192,21 @@ namespace big m_state["package"]["path"] = scripts_search_path; } + static std::optional make_absolute(const std::filesystem::path& root, const std::filesystem::path& user_path) + { + if (user_path.is_absolute()) + return std::nullopt; + + auto canon_root = std::filesystem::weakly_canonical(root); + auto final_path = std::filesystem::weakly_canonical(canon_root / user_path); + auto [root_end, nothing] = std::mismatch(canon_root.begin(), canon_root.end(), final_path.begin()); + + if (root_end != canon_root.end()) + return std::nullopt; + + return final_path; + }; + void lua_module::sandbox_lua_os_library() { const auto& os = m_state["os"]; @@ -189,22 +216,43 @@ namespace big sandbox_os["date"] = os["date"]; sandbox_os["difftime"] = os["difftime"]; sandbox_os["time"] = os["time"]; + + // Lua API: Function + // Table: os + // Name: rename + // Param: oldname: string + // Param: newname: string + // Returns: boolean, string?: True if the file was successfully renamed, false and an error message otherwise. + sandbox_os["rename"] = [this](const std::string& oldname, const std::string& newname) -> sol::object { + const auto old_path = make_absolute(get_config_folder(), oldname); + const auto new_path = make_absolute(get_config_folder(), newname); + + if (!old_path) + { + LOG(WARNING) << "os.rename is restricted to the script's config folder, and the filename provided (" << oldname << ") seems to be outside of it."; + return sol::make_object(m_state, std::make_tuple(false, "File not found.")); + } + + if (!new_path) + { + LOG(WARNING) << "os.rename is restricted to the script's config folder, and the filename provided (" << newname << ") seems to be outside of it."; + return sol::make_object(m_state, std::make_tuple(false, "New file name is invalid.")); + } + + try + { + std::filesystem::rename(old_path.value(), new_path.value()); + return sol::make_object(m_state, true); + } + catch (const std::exception& e) + { + return sol::make_object(m_state, std::make_tuple(false, e.what())); + } + }; m_state["os"] = sandbox_os; } - static std::optional make_absolute(const std::filesystem::path& root, const std::filesystem::path& user_path) - { - auto final_path = std::filesystem::weakly_canonical(root / user_path); - - auto [root_end, nothing] = std::mismatch(root.begin(), root.end(), final_path.begin()); - - if (root_end != root.end()) - return std::nullopt; - - return final_path; - }; - void lua_module::sandbox_lua_io_library() { auto io = m_state["io"]; @@ -221,7 +269,7 @@ namespace big // Name: open // Returns: file_handle: file handle or nil if can't read / write to the given path. sandbox_io["open"] = [this](const std::string& filename, const std::string& mode) { - const auto scripts_config_sub_path = make_absolute(g_lua_manager->get_scripts_config_folder().get_path(), filename); + const auto scripts_config_sub_path = make_absolute(get_config_folder(), filename); if (!scripts_config_sub_path) { LOG(WARNING) << "io.open is restricted to the scripts_config folder, and the filename provided (" << filename << ") is outside of it."; @@ -242,9 +290,10 @@ namespace big // Lua API: Function // Table: io // Name: exists + // Param: filename: string // Returns: boolean: True if the passed file path exists - sandbox_io["exists"] = [](const std::string& filename) -> bool { - const auto scripts_config_sub_path = make_absolute(g_lua_manager->get_scripts_config_folder().get_path(), filename); + sandbox_io["exists"] = [this](const std::string& filename) -> bool { + const auto scripts_config_sub_path = make_absolute(get_config_folder(), filename); if (!scripts_config_sub_path) { LOG(WARNING) << "io.open is restricted to the scripts_config folder, and the filename provided (" << filename << ") is outside of it."; diff --git a/src/lua/lua_module.hpp b/src/lua/lua_module.hpp index 6eef53ab..d2aa09b2 100644 --- a/src/lua/lua_module.hpp +++ b/src/lua/lua_module.hpp @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "../script.hpp" #include "bindings/gui/gui_element.hpp" #include "core/data/menu_event.hpp" @@ -60,6 +60,7 @@ namespace big // used for sandboxing and limiting to only our custom search path for the lua require function void set_folder_for_lua_require(folder& scripts_folder); + const std::filesystem::path get_config_folder() const; void sandbox_lua_os_library(); void sandbox_lua_io_library();