diff --git a/src/controllers/plugins/ApiChatterino.cpp b/src/controllers/plugins/ApiChatterino.cpp index fd61cff00..f008d24ed 100644 --- a/src/controllers/plugins/ApiChatterino.cpp +++ b/src/controllers/plugins/ApiChatterino.cpp @@ -13,6 +13,9 @@ # include "lauxlib.h" # include "lua.h" # include "lualib.h" + +# include +# include namespace chatterino::lua::api { int c2_register_command(lua_State *L) @@ -135,9 +138,78 @@ int g_load(lua_State *L) } lua_call(L, countArgs, LUA_MULTRET); - qCDebug(chatterinoLua) << "FDM " << lua_gettop(L); return lua_gettop(L); } + +int g_dofile(lua_State *L) +{ + auto countArgs = lua_gettop(L); + // Lua allows dofile() which loads from stdin, but this is very useless in our case + if (countArgs == 0) + { + lua_pushnil(L); + // NOLINTNEXTLINE + luaL_error(L, "it is not allowed to call dofile() without arguments"); + return 1; + } + + auto *pl = getApp()->plugins->getPluginByStatePtr(L); + QString fname; + if (!lua::pop(L, &fname)) + { + lua_pushnil(L); + // NOLINTNEXTLINE + luaL_error(L, "chatterino g_dofile: expected a string for a filename"); + return 1; + } + auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/"); + auto file = dir.resolved(fname); + + qCDebug(chatterinoLua) << "plugin" << pl->codename << "is trying to load" + << file << "(its dir is" << dir << ")"; + if (!dir.isParentOf(file)) + { + lua_pushnil(L); + // NOLINTNEXTLINE + luaL_error(L, "chatterino g_dofile: filename must be inside of the " + "plugin directory"); + return 1; + } + + auto path = file.path(QUrl::FullyDecoded); + // validate utf-8 to block bytecode exploits + QFile qf(path); + qf.open(QIODevice::ReadOnly); + if (qf.size() > 10'000'000) + { + lua_pushnil(L); + // NOLINTNEXTLINE + luaL_error(L, "chatterino g_dofile: size limit of 10MB exceeded, what " + "the hell are you doing"); + return 1; + } + auto data = qf.readAll(); + auto *utf8 = QTextCodec::codecForName("UTF-8"); + QTextCodec::ConverterState state; + utf8->toUnicode(data.constData(), data.size(), &state); + if (state.invalidChars != 0) + { + lua_pushnil(L); + // NOLINTNEXTLINE + luaL_error(L, "invalid utf-8 in dofile() target (%s) is not allowed", + fname.toStdString().c_str()); + return 1; + } + + // fetch dofile and call it + lua_getfield(L, LUA_REGISTRYINDEX, "real_dofile"); + // maybe data race here if symlink was swapped? + lua::push(L, path); + lua_call(L, 1, LUA_MULTRET); + + return lua_gettop(L); +} + } // namespace chatterino::lua::api #endif diff --git a/src/controllers/plugins/ApiChatterino.hpp b/src/controllers/plugins/ApiChatterino.hpp index 2c6dd7ac4..186d08f8c 100644 --- a/src/controllers/plugins/ApiChatterino.hpp +++ b/src/controllers/plugins/ApiChatterino.hpp @@ -9,7 +9,8 @@ int c2_register_command(lua_State *L); // NOLINT(readability-identifier-naming) int c2_send_msg(lua_State *L); // NOLINT(readability-identifier-naming) int c2_system_msg(lua_State *L); // NOLINT(readability-identifier-naming) -int g_load(lua_State *L); // NOLINT(readability-identifier-naming) +int g_load(lua_State *L); // NOLINT(readability-identifier-naming) +int g_dofile(lua_State *L); // NOLINT(readability-identifier-naming) } // namespace chatterino::lua::api diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 86c2ded5b..dfd339239 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -72,6 +72,11 @@ public: std::set listRegisteredCommands(); + const QDir &loadDirectory() const + { + return this->loadDirectory_; + } + private: QDir loadDirectory_; lua_State *state_; diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 83920357f..ba7bb3bbf 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -165,13 +165,25 @@ void PluginController::openLibrariesFor(lua_State *L, // possibly randomize this name at runtime to prevent some attacks? lua_setfield(L, LUA_REGISTRYINDEX, "real_load"); + lua_getfield(L, gtable, "dofile"); + lua_setfield(L, LUA_REGISTRYINDEX, "real_dofile"); + // NOLINTNEXTLINE static const luaL_Reg replacementFuncs[] = { {"load", lua::api::g_load}, + + // chatterino dofile is way more similar to require() than dofile() + {"execfile", lua::api::g_dofile}, {nullptr, nullptr}, }; luaL_setfuncs(L, replacementFuncs, 0); + lua_pushnil(L); + lua_setfield(L, gtable, "loadfile"); + + lua_pushnil(L); + lua_setfield(L, gtable, "dofile"); + lua_pop(L, 1); }