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.
This commit is contained in:
SAMURAI
2026-03-17 18:09:23 +01:00
committed by GitHub
parent dda0daff24
commit f41a378e21
3 changed files with 108 additions and 24 deletions
+36 -2
View File
@@ -1,4 +1,4 @@
#include "lua_manager.hpp" #include "lua_manager.hpp"
#include "file_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_module> lua_manager::load_module(const std::filesystem::path& module_path) std::weak_ptr<lua_module> lua_manager::load_module(const std::filesystem::path& module_path)
{ {
if (!std::filesystem::exists(module_path)) if (!std::filesystem::exists(module_path))
@@ -321,7 +349,13 @@ namespace big
return {}; return {};
const auto module_name = module_path.filename().string(); 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); std::lock_guard guard(m_module_lock);
for (const auto& module : m_modules) for (const auto& module : m_modules)
+70 -21
View File
@@ -1,4 +1,4 @@
#include "lua_module.hpp" #include "lua_module.hpp"
#include "bindings/command.hpp" #include "bindings/command.hpp"
#include "bindings/entities.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) : lua_module::lua_module(const std::filesystem::path& module_path, folder& scripts_folder, bool disabled) :
m_state(), m_state(),
m_module_path(module_path), m_module_path(module_path),
m_module_name(module_path.filename().string()), m_module_name(module_path.filename().string()),
m_module_id(rage::joaat(m_module_name)), m_module_id(rage::joaat(m_module_name)),
m_disabled(disabled) m_disabled(disabled)
{ {
if (!m_disabled) if (!m_disabled)
{ {
@@ -163,6 +163,15 @@ namespace big
return m_disabled; 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) void lua_module::set_folder_for_lua_require(folder& scripts_folder)
{ {
std::string scripts_search_path = scripts_folder.get_path().string() + "/?.lua;"; std::string scripts_search_path = scripts_folder.get_path().string() + "/?.lua;";
@@ -172,6 +181,9 @@ namespace big
if (!entry.is_directory()) if (!entry.is_directory())
continue; continue;
if (std::filesystem::relative(entry, scripts_folder.get_path()).wstring().contains(L"disabled"))
continue;
scripts_search_path += entry.path().string() + "/?.lua;"; scripts_search_path += entry.path().string() + "/?.lua;";
} }
// Remove final ';' // Remove final ';'
@@ -180,6 +192,21 @@ namespace big
m_state["package"]["path"] = scripts_search_path; m_state["package"]["path"] = scripts_search_path;
} }
static std::optional<std::filesystem::path> 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() void lua_module::sandbox_lua_os_library()
{ {
const auto& os = m_state["os"]; const auto& os = m_state["os"];
@@ -189,22 +216,43 @@ namespace big
sandbox_os["date"] = os["date"]; sandbox_os["date"] = os["date"];
sandbox_os["difftime"] = os["difftime"]; sandbox_os["difftime"] = os["difftime"];
sandbox_os["time"] = os["time"]; 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; m_state["os"] = sandbox_os;
} }
static std::optional<std::filesystem::path> 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() void lua_module::sandbox_lua_io_library()
{ {
auto io = m_state["io"]; auto io = m_state["io"];
@@ -221,7 +269,7 @@ namespace big
// Name: open // Name: open
// Returns: file_handle: file handle or nil if can't read / write to the given path. // 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) { 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) 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."; 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 // Lua API: Function
// Table: io // Table: io
// Name: exists // Name: exists
// Param: filename: string
// Returns: boolean: True if the passed file path exists // Returns: boolean: True if the passed file path exists
sandbox_io["exists"] = [](const std::string& filename) -> bool { sandbox_io["exists"] = [this](const std::string& filename) -> bool {
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) 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."; LOG(WARNING) << "io.open is restricted to the scripts_config folder, and the filename provided (" << filename << ") is outside of it.";
+2 -1
View File
@@ -1,4 +1,4 @@
#pragma once #pragma once
#include "../script.hpp" #include "../script.hpp"
#include "bindings/gui/gui_element.hpp" #include "bindings/gui/gui_element.hpp"
#include "core/data/menu_event.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 // 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); 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_os_library();
void sandbox_lua_io_library(); void sandbox_lua_io_library();