mirror of
https://github.com/minetest/minetest.git
synced 2024-12-23 06:32:23 +01:00
Refactor ScriptApiSecurity for cleaner separation of concerns
This commit is contained in:
parent
4c44942a39
commit
1fd4e0b82d
@ -3,6 +3,7 @@
|
|||||||
// Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
|
// Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
|
||||||
|
|
||||||
#include "common/c_internal.h"
|
#include "common/c_internal.h"
|
||||||
|
#include "cpp_api/s_security.h"
|
||||||
#include "util/numeric.h"
|
#include "util/numeric.h"
|
||||||
#include "debug.h"
|
#include "debug.h"
|
||||||
#include "log.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)
|
void call_string_dump(lua_State *L, int idx)
|
||||||
{
|
{
|
||||||
// Retrieve string.dump from insecure env to avoid it being tampered with
|
// Retrieve string.dump from untampered env
|
||||||
lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP);
|
ScriptApiSecurity::getGlobalsBackup(L);
|
||||||
if (!lua_isnil(L, -1))
|
lua_getfield(L, -1, "string");
|
||||||
lua_getfield(L, -1, "string");
|
|
||||||
else
|
|
||||||
lua_getglobal(L, "string");
|
|
||||||
lua_getfield(L, -1, "dump");
|
lua_getfield(L, -1, "dump");
|
||||||
lua_remove(L, -2); // remove _G
|
lua_remove(L, -2); // remove _G
|
||||||
lua_remove(L, -2); // remove 'string' table
|
lua_remove(L, -2); // remove 'string' table
|
||||||
|
@ -39,6 +39,7 @@ enum {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
CUSTOM_RIDX_SCRIPTAPI,
|
CUSTOM_RIDX_SCRIPTAPI,
|
||||||
|
/// @warning don't use directly, `ScriptApiSecurity` has wrappers
|
||||||
CUSTOM_RIDX_GLOBALS_BACKUP,
|
CUSTOM_RIDX_GLOBALS_BACKUP,
|
||||||
CUSTOM_RIDX_CURRENT_MOD_NAME,
|
CUSTOM_RIDX_CURRENT_MOD_NAME,
|
||||||
CUSTOM_RIDX_ERROR_HANDLER,
|
CUSTOM_RIDX_ERROR_HANDLER,
|
||||||
|
@ -50,11 +50,17 @@ class AsyncWorkerThread : public Thread,
|
|||||||
public:
|
public:
|
||||||
virtual ~AsyncWorkerThread();
|
virtual ~AsyncWorkerThread();
|
||||||
|
|
||||||
void *run();
|
void *run() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AsyncWorkerThread(AsyncEngine* jobDispatcher, const std::string &name);
|
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:
|
private:
|
||||||
AsyncEngine *jobDispatcher = nullptr;
|
AsyncEngine *jobDispatcher = nullptr;
|
||||||
bool isErrored = false;
|
bool isErrored = false;
|
||||||
|
@ -225,37 +225,6 @@ std::string ScriptApiBase::getCurrentModNameInsecure(lua_State *L)
|
|||||||
return ret;
|
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,
|
void ScriptApiBase::loadMod(const std::string &script_path,
|
||||||
const std::string &mod_name)
|
const std::string &mod_name)
|
||||||
{
|
{
|
||||||
@ -273,7 +242,7 @@ void ScriptApiBase::loadScript(const std::string &script_path)
|
|||||||
int error_handler = PUSH_ERROR_HANDLER(L);
|
int error_handler = PUSH_ERROR_HANDLER(L);
|
||||||
|
|
||||||
bool ok;
|
bool ok;
|
||||||
if (m_secure) {
|
if (ScriptApiSecurity::isSecure(L)) {
|
||||||
ok = ScriptApiSecurity::safeLoadFile(L, script_path.c_str());
|
ok = ScriptApiSecurity::safeLoadFile(L, script_path.c_str());
|
||||||
} else {
|
} else {
|
||||||
ok = !luaL_loadfile(L, script_path.c_str());
|
ok = !luaL_loadfile(L, script_path.c_str());
|
||||||
|
@ -101,17 +101,15 @@ public:
|
|||||||
void setOriginDirect(const char *origin);
|
void setOriginDirect(const char *origin);
|
||||||
void setOriginFromTableRaw(int index, const char *fxn);
|
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,
|
* Returns the currently running mod, only during init time.
|
||||||
// so the boundary of who is responsible is fuzzy.
|
* The reason this is insecure is that mods can mess with each others code,
|
||||||
// Note: checking this against BUILTIN_MOD_NAME is always safe (not spoofable).
|
* so the boundary of who is responsible is fuzzy.
|
||||||
// returns "" on error
|
* @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);
|
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()
|
#if !CHECK_CLIENT_BUILD()
|
||||||
inline void clientOpenLibs(lua_State *L) { assert(false); }
|
inline void clientOpenLibs(lua_State *L) { assert(false); }
|
||||||
@ -171,7 +169,7 @@ protected:
|
|||||||
|
|
||||||
std::recursive_mutex m_luastackmutex;
|
std::recursive_mutex m_luastackmutex;
|
||||||
std::string m_last_run_mod;
|
std::string m_last_run_mod;
|
||||||
bool m_secure = false;
|
|
||||||
#ifdef SCRIPTAPI_LOCK_DEBUG
|
#ifdef SCRIPTAPI_LOCK_DEBUG
|
||||||
int m_lock_recursion_count{};
|
int m_lock_recursion_count{};
|
||||||
std::thread::id m_owning_thread;
|
std::thread::id m_owning_thread;
|
||||||
|
@ -260,6 +260,8 @@ void ScriptApiSecurity::initializeSecurity()
|
|||||||
lua_pop(L, 1); // Pop empty string
|
lua_pop(L, 1); // Pop empty string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if CHECK_CLIENT_BUILD()
|
||||||
|
|
||||||
void ScriptApiSecurity::initializeSecurityClient()
|
void ScriptApiSecurity::initializeSecurityClient()
|
||||||
{
|
{
|
||||||
static const char *whitelist[] = {
|
static const char *whitelist[] = {
|
||||||
@ -375,6 +377,8 @@ void ScriptApiSecurity::initializeSecurityClient()
|
|||||||
setLuaEnv(L, thread);
|
setLuaEnv(L, thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
int ScriptApiSecurity::getThread(lua_State *L)
|
int ScriptApiSecurity::getThread(lua_State *L)
|
||||||
{
|
{
|
||||||
#if LUA_VERSION_NUM <= 501
|
#if LUA_VERSION_NUM <= 501
|
||||||
@ -408,19 +412,24 @@ void ScriptApiSecurity::setLuaEnv(lua_State *L, int thread)
|
|||||||
|
|
||||||
bool ScriptApiSecurity::isSecure(lua_State *L)
|
bool ScriptApiSecurity::isSecure(lua_State *L)
|
||||||
{
|
{
|
||||||
#if CHECK_CLIENT_BUILD()
|
auto *script = ModApiBase::getScriptApiBase(L);
|
||||||
auto script = ModApiBase::getScriptApiBase(L);
|
if (auto *sec = dynamic_cast<ScriptApiSecurity*>(script))
|
||||||
// CSM keeps no globals backup but is always secure
|
return sec->m_secure;
|
||||||
if (script->getType() == ScriptingType::Client)
|
return false;
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP);
|
|
||||||
bool secure = !lua_isnil(L, -1);
|
|
||||||
lua_pop(L, 1);
|
|
||||||
return secure;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]) {
|
if (code.size() > 0 && code[0] == LUA_SIGNATURE[0]) {
|
||||||
lua_pushliteral(L, "Bytecode prohibited when mod security is enabled.");
|
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;
|
fp = stdin;
|
||||||
chunk_name = const_cast<char *>("=stdin");
|
chunk_name = const_cast<char *>("=stdin");
|
||||||
} else {
|
} else {
|
||||||
fp = fopen(path, "rb");
|
fp = std::fopen(path, "rb");
|
||||||
if (!fp) {
|
if (!fp) {
|
||||||
lua_pushfstring(L, "%s: %s", path, strerror(errno));
|
lua_pushfstring(L, "%s: %s", path, strerror(errno));
|
||||||
return false;
|
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<ScriptApiSecurity*>(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."));
|
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)
|
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);
|
return checkModNameWhitelisted(mod_name, setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -528,16 +565,8 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path,
|
|||||||
if (write_allowed)
|
if (write_allowed)
|
||||||
*write_allowed = false;
|
*write_allowed = false;
|
||||||
|
|
||||||
std::string str; // Transient
|
|
||||||
|
|
||||||
std::string abs_path = fs::AbsolutePath(path);
|
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
|
// If we couldn't find the absolute path (path doesn't exist) then
|
||||||
// try removing the last components until it works (to allow
|
// try removing the last components until it works (to allow
|
||||||
// non-existent files/folders for mkdir).
|
// non-existent files/folders for mkdir).
|
||||||
@ -560,61 +589,84 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path,
|
|||||||
}
|
}
|
||||||
if (abs_path.empty())
|
if (abs_path.empty())
|
||||||
return false;
|
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.
|
// directory in worldmods if worldmods doesn't exist.
|
||||||
if (!removed.empty())
|
if (!removed.empty())
|
||||||
abs_path += DIR_DELIM + removed;
|
abs_path += DIR_DELIM + removed;
|
||||||
|
|
||||||
// Get gamedef from registry
|
tracestream << "ScriptApiSecurity: path \"" << path << "\" resolved to \""
|
||||||
ScriptApiBase *script = ModApiBase::getScriptApiBase(L);
|
<< abs_path << "\"" << std::endl;
|
||||||
const IGameDef *gamedef = script->getGameDef();
|
|
||||||
|
// Ask the environment-specific implementation
|
||||||
|
auto *sec = ModApiBase::getScriptApi<ScriptApiSecurity>(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)
|
if (!gamedef)
|
||||||
return false;
|
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
|
// Get mod name
|
||||||
std::string mod_name = ScriptApiBase::getCurrentModNameInsecure(L);
|
std::string mod_name = ScriptApiBase::getCurrentModNameInsecure(L);
|
||||||
if (!mod_name.empty()) {
|
if (!mod_name.empty()) {
|
||||||
// Builtin can access anything
|
// Builtin can access anything
|
||||||
if (mod_name == BUILTIN_MOD_NAME) {
|
if (mod_name == BUILTIN_MOD_NAME) {
|
||||||
if (write_allowed) *write_allowed = true;
|
set_write_allowed(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Allow paths in mod path
|
// Allow paths in mod path
|
||||||
// Don't bother if write access isn't important, since it will be handled later
|
// Don't bother if write access isn't important, since it will be handled later
|
||||||
if (write_required || write_allowed != NULL) {
|
if (write_required || write_allowed) {
|
||||||
const ModSpec *mod = gamedef->getModSpec(mod_name);
|
const ModSpec *mod = gamedef->getModSpec(mod_name);
|
||||||
if (mod) {
|
if (mod) {
|
||||||
str = fs::AbsolutePath(mod->path);
|
str = fs::AbsolutePath(mod->path);
|
||||||
if (!str.empty() && fs::PathStartsWith(abs_path, str)) {
|
if (!str.empty() && fs::PathStartsWith(abs_path, str)) {
|
||||||
// `mod_name` cannot be trusted here, so we catch the scenarios where this becomes a problem:
|
// `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") ||
|
bool is_trusted = checkModNameWhitelisted(mod_name, "secure.trusted_mods") ||
|
||||||
checkModNameWhitelisted(mod_name, "secure.http_mods");
|
checkModNameWhitelisted(mod_name, "secure.http_mods");
|
||||||
std::string filename = lowercase(fs::GetFilenameFromPath(abs_path.c_str()));
|
std::string filename = lowercase(fs::GetFilenameFromPath(abs_path.c_str()));
|
||||||
// By writing to any of these a malicious mod could turn itself into
|
// By writing to any of these a malicious mod could turn itself into
|
||||||
// an existing trusted mod by renaming or becoming a modpack.
|
// an existing trusted mod by renaming or becoming a modpack.
|
||||||
bool is_dangerous_file = filename == "mod.conf" ||
|
bool is_dangerous_file = filename == "mod.conf" ||
|
||||||
filename == "modpack.conf" ||
|
filename == "modpack.conf" ||
|
||||||
filename == "modpack.txt";
|
filename == "modpack.txt";
|
||||||
if (write_required) {
|
if (write_required) {
|
||||||
if (is_trusted) {
|
if (is_trusted) {
|
||||||
throw LuaError(
|
throw LuaError(
|
||||||
"Unable to write to a trusted or http mod's directory. "
|
"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.");
|
"For data storage consider minetest.get_mod_data_path() or minetest.get_worldpath() instead.");
|
||||||
} else if (is_dangerous_file) {
|
} else if (is_dangerous_file) {
|
||||||
throw LuaError(
|
throw LuaError(
|
||||||
"Unable to write to special file for security reasons");
|
"Unable to write to special file for security reasons");
|
||||||
} else {
|
} else {
|
||||||
const char *message =
|
const char *message =
|
||||||
"Writing to mod directories is deprecated, as any changes "
|
"Writing to mod directories is deprecated, as any changes "
|
||||||
"will be overwritten when updating content. "
|
"will be overwritten when updating content. "
|
||||||
"For data storage consider minetest.get_mod_data_path() or minetest.get_worldpath() instead.";
|
"For data storage consider minetest.get_mod_data_path() or minetest.get_worldpath() instead.";
|
||||||
log_deprecated(L, message, 1);
|
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();
|
const SubgameSpec *game_spec = gamedef->getGameSpec();
|
||||||
if (game_spec && !game_spec->path.empty()) {
|
if (game_spec && !game_spec->path.empty()) {
|
||||||
str = fs::AbsolutePath(game_spec->path);
|
str = fs::AbsolutePath(game_spec->path);
|
||||||
if (!str.empty() && fs::PathStartsWith(abs_path, str)) {
|
if (!str.empty() && fs::PathStartsWith(abs_path, str))
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -635,16 +686,15 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path,
|
|||||||
const std::vector<ModSpec> &mods = gamedef->getMods();
|
const std::vector<ModSpec> &mods = gamedef->getMods();
|
||||||
for (const ModSpec &mod : mods) {
|
for (const ModSpec &mod : mods) {
|
||||||
str = fs::AbsolutePath(mod.path);
|
str = fs::AbsolutePath(mod.path);
|
||||||
if (!str.empty() && fs::PathStartsWith(abs_path, str)) {
|
if (!str.empty() && fs::PathStartsWith(abs_path, str))
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow read/write access to all mod common dirs
|
// Allow read/write access to all mod common dirs
|
||||||
str = fs::AbsolutePath(gamedef->getModDataPath());
|
str = fs::AbsolutePath(gamedef->getModDataPath());
|
||||||
if (!str.empty() && fs::PathStartsWith(abs_path, str)) {
|
if (!str.empty() && fs::PathStartsWith(abs_path, str)) {
|
||||||
if (write_allowed) *write_allowed = true;
|
set_write_allowed(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -662,12 +712,11 @@ bool ScriptApiSecurity::checkPath(lua_State *L, const char *path,
|
|||||||
}
|
}
|
||||||
// Allow all other paths in world path
|
// Allow all other paths in world path
|
||||||
if (fs::PathStartsWith(abs_path, str)) {
|
if (fs::PathStartsWith(abs_path, str)) {
|
||||||
if (write_allowed) *write_allowed = true;
|
set_write_allowed(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to disallowing
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,10 +12,12 @@
|
|||||||
throw LuaError(std::string("Mod security: Blocked attempted ") + \
|
throw LuaError(std::string("Mod security: Blocked attempted ") + \
|
||||||
(write_required ? "write to " : "read from ") + path); \
|
(write_required ? "write to " : "read from ") + path); \
|
||||||
}
|
}
|
||||||
|
|
||||||
#define CHECK_SECURE_PATH(L, path, write_required) \
|
#define CHECK_SECURE_PATH(L, path, write_required) \
|
||||||
if (ScriptApiSecurity::isSecure(L)) { \
|
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) \
|
#define CHECK_SECURE_PATH_POSSIBLE_WRITE(L, path, ptr) \
|
||||||
if (ScriptApiSecurity::isSecure(L)) { \
|
if (ScriptApiSecurity::isSecure(L)) { \
|
||||||
CHECK_SECURE_PATH_INTERNAL(L, path, false, ptr); \
|
CHECK_SECURE_PATH_INTERNAL(L, path, false, ptr); \
|
||||||
@ -27,19 +29,65 @@ class ScriptApiSecurity : virtual public ScriptApiBase
|
|||||||
public:
|
public:
|
||||||
// Sets up security on the ScriptApi's Lua state
|
// Sets up security on the ScriptApi's Lua state
|
||||||
void initializeSecurity();
|
void initializeSecurity();
|
||||||
|
#if CHECK_CLIENT_BUILD()
|
||||||
void initializeSecurityClient();
|
void initializeSecurityClient();
|
||||||
|
#else
|
||||||
|
inline void initializeSecurityClient() { assert(0); }
|
||||||
|
#endif
|
||||||
|
|
||||||
// Checks if the Lua state has been secured
|
// Checks if the Lua state has been secured
|
||||||
static bool isSecure(lua_State *L);
|
static bool isSecure(lua_State *L);
|
||||||
// Loads a string as Lua code safely (doesn't allow bytecode).
|
// Leaves the untampered globals (table) on top of the stack
|
||||||
static bool safeLoadString(lua_State *L, const std::string &code, const char *chunk_name);
|
static void getGlobalsBackup(lua_State *L);
|
||||||
// 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);
|
/// Loads a string as Lua code safely (doesn't allow bytecode).
|
||||||
// Check if mod is whitelisted in the given setting
|
static bool safeLoadString(lua_State *L, std::string_view code, const char *chunk_name);
|
||||||
// This additionally checks that the mod's main file scope is executing.
|
/// 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);
|
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,
|
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:
|
private:
|
||||||
int getThread(lua_State *L);
|
int getThread(lua_State *L);
|
||||||
@ -48,6 +96,8 @@ private:
|
|||||||
// creates an empty Lua environment
|
// creates an empty Lua environment
|
||||||
void createEmptyEnv(lua_State *L);
|
void createEmptyEnv(lua_State *L);
|
||||||
|
|
||||||
|
bool m_secure = false;
|
||||||
|
|
||||||
// Syntax: "sl_" <Library name or 'g' (global)> '_' <Function name>
|
// Syntax: "sl_" <Library name or 'g' (global)> '_' <Function name>
|
||||||
// (sl stands for Secure Lua)
|
// (sl stands for Secure Lua)
|
||||||
|
|
||||||
|
@ -494,18 +494,13 @@ int ModApiUtil::l_request_insecure_environment(lua_State *L)
|
|||||||
{
|
{
|
||||||
NO_MAP_LOCK_REQUIRED;
|
NO_MAP_LOCK_REQUIRED;
|
||||||
|
|
||||||
// Just return _G if security is disabled
|
if (ScriptApiSecurity::isSecure(L)) {
|
||||||
if (!ScriptApiSecurity::isSecure(L)) {
|
if (!ScriptApiSecurity::checkWhitelisted(L, "secure.trusted_mods"))
|
||||||
lua_getglobal(L, "_G");
|
return 0;
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ScriptApiSecurity::checkWhitelisted(L, "secure.trusted_mods")) {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push insecure environment
|
// Push insecure environment
|
||||||
lua_rawgeti(L, LUA_REGISTRYINDEX, CUSTOM_RIDX_GLOBALS_BACKUP);
|
ScriptApiSecurity::getGlobalsBackup(L);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
#include "cpp_api/s_base.h"
|
#include "cpp_api/s_base.h"
|
||||||
#include "cpp_api/s_client.h"
|
#include "cpp_api/s_client.h"
|
||||||
#include "cpp_api/s_modchannels.h"
|
#include "cpp_api/s_modchannels.h"
|
||||||
@ -27,6 +29,16 @@ public:
|
|||||||
void on_camera_ready(Camera *camera);
|
void on_camera_ready(Camera *camera);
|
||||||
void on_minimap_ready(Minimap *minimap);
|
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:
|
private:
|
||||||
virtual void InitializeModApi(lua_State *L, int top);
|
virtual void InitializeModApi(lua_State *L, int top);
|
||||||
};
|
};
|
||||||
|
@ -17,6 +17,13 @@ class EmergeScripting:
|
|||||||
public:
|
public:
|
||||||
EmergeScripting(EmergeThread *parent);
|
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:
|
private:
|
||||||
void InitializeModApi(lua_State *L, int top);
|
void InitializeModApi(lua_State *L, int top);
|
||||||
};
|
};
|
||||||
|
@ -50,6 +50,15 @@ public:
|
|||||||
u32 queueAsync(std::string &&serialized_func,
|
u32 queueAsync(std::string &&serialized_func,
|
||||||
PackedValue *param, const std::string &mod_origin);
|
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:
|
private:
|
||||||
void InitializeModApi(lua_State *L, int top);
|
void InitializeModApi(lua_State *L, int top);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user