diff options
author | Oran Agra <oran@redislabs.com> | 2022-04-27 16:32:17 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-27 16:32:17 +0300 |
commit | d375595d5e3ae2e5c29e6c00a2dc3d60578fd9fc (patch) | |
tree | c4d753d3ee0109e3513a879af8c5487e002d10a3 /src/script_lua.c | |
parent | fb4e0d400ff82117104bde5296c477ad95f8dd41 (diff) | |
parent | c1f3020631ea8640f2b6aa666a245dd76635a807 (diff) | |
download | redis-7.0.0.tar.gz |
Merge pull request #10652 from oranagra/redis-7.0.07.0.0
Redis 7.0.0
Diffstat (limited to 'src/script_lua.c')
-rw-r--r-- | src/script_lua.c | 318 |
1 files changed, 213 insertions, 105 deletions
diff --git a/src/script_lua.c b/src/script_lua.c index 9a08a7e47..36868ec2b 100644 --- a/src/script_lua.c +++ b/src/script_lua.c @@ -41,6 +41,97 @@ #include <ctype.h> #include <math.h> +/* Globals that are added by the Lua libraries */ +static char *libraries_allow_list[] = { + "string", + "cjson", + "bit", + "cmsgpack", + "math", + "table", + "struct", + NULL, +}; + +/* Redis Lua API globals */ +static char *redis_api_allow_list[] = { + "redis", + "__redis__err__handler", /* error handler for eval, currently located on globals. + Should move to registry. */ + NULL, +}; + +/* Lua builtins */ +static char *lua_builtins_allow_list[] = { + "xpcall", + "tostring", + "getfenv", + "setmetatable", + "next", + "assert", + "tonumber", + "rawequal", + "collectgarbage", + "getmetatable", + "rawset", + "pcall", + "coroutine", + "type", + "_G", + "select", + "unpack", + "gcinfo", + "pairs", + "rawget", + "loadstring", + "ipairs", + "_VERSION", + "setfenv", + "load", + "error", + NULL, +}; + +/* Lua builtins which are not documented on the Lua documentation */ +static char *lua_builtins_not_documented_allow_list[] = { + "newproxy", + NULL, +}; + +/* Lua builtins which are allowed on initialization but will be removed right after */ +static char *lua_builtins_removed_after_initialization_allow_list[] = { + "debug", /* debug will be set to nil after the error handler will be created */ + NULL, +}; + +/* Those allow lists was created from the globals that was + * available to the user when the allow lists was first introduce. + * Because we do not want to break backward compatibility we keep + * all the globals. The allow lists will prevent us from accidentally + * creating unwanted globals in the future. + * + * Also notice that the allow list is only checked on start time, + * after that the global table is locked so not need to check anything.*/ +static char **allow_lists[] = { + libraries_allow_list, + redis_api_allow_list, + lua_builtins_allow_list, + lua_builtins_not_documented_allow_list, + lua_builtins_removed_after_initialization_allow_list, + NULL, +}; + +/* Deny list contains elements which we know we do not want to add to globals + * and there is no need to print a warning message form them. We will print a + * log message only if an element was added to the globals and the element is + * not on the allow list nor on the back list. */ +static char *deny_list[] = { + "dofile", + "loadfile", + "print", + NULL, +}; + static int redis_math_random (lua_State *L); static int redis_math_randomseed (lua_State *L); static void redisProtocolToLuaType_Int(void *ctx, long long val, const char *proto, size_t proto_len); @@ -1113,15 +1204,6 @@ static void luaLoadLibraries(lua_State *lua) { #endif } -/* Remove a functions that we don't want to expose to the Redis scripting - * environment. */ -static void luaRemoveUnsupportedFunctions(lua_State *lua) { - lua_pushnil(lua); - lua_setglobal(lua,"loadfile"); - lua_pushnil(lua); - lua_setglobal(lua,"dofile"); -} - /* Return sds of the string value located on stack at the given index. * Return NULL if the value is not a string. */ sds luaGetStringSds(lua_State *lua, int index) { @@ -1135,107 +1217,120 @@ sds luaGetStringSds(lua_State *lua, int index) { return str_sds; } -/* This function installs metamethods in the global table _G that prevent - * the creation of globals accidentally. - * - * It should be the last to be called in the scripting engine initialization - * sequence, because it may interact with creation of globals. - * - * On Legacy Lua (eval) we need to check 'w ~= \"main\"' otherwise we will not be able - * to create the global 'function <sha> ()' variable. On Functions Lua engine we do not use - * this trick so it's not needed. */ -void luaEnableGlobalsProtection(lua_State *lua, int is_eval) { - char *s[32]; - sds code = sdsempty(); - int j = 0; - - /* strict.lua from: http://metalua.luaforge.net/src/lib/strict.lua.html. - * Modified to be adapted to Redis. */ - s[j++]="local dbg=debug\n"; - s[j++]="local mt = {}\n"; - s[j++]="setmetatable(_G, mt)\n"; - s[j++]="mt.__newindex = function (t, n, v)\n"; - s[j++]=" if dbg.getinfo(2) then\n"; - s[j++]=" local w = dbg.getinfo(2, \"S\").what\n"; - s[j++]= is_eval ? " if w ~= \"main\" and w ~= \"C\" then\n" : " if w ~= \"C\" then\n"; - s[j++]=" error(\"Script attempted to create global variable '\"..tostring(n)..\"'\", 2)\n"; - s[j++]=" end\n"; - s[j++]=" end\n"; - s[j++]=" rawset(t, n, v)\n"; - s[j++]="end\n"; - s[j++]="mt.__index = function (t, n)\n"; - s[j++]=" if dbg.getinfo(2) and dbg.getinfo(2, \"S\").what ~= \"C\" then\n"; - s[j++]=" error(\"Script attempted to access nonexistent global variable '\"..tostring(n)..\"'\", 2)\n"; - s[j++]=" end\n"; - s[j++]=" return rawget(t, n)\n"; - s[j++]="end\n"; - s[j++]="debug = nil\n"; - s[j++]=NULL; - - for (j = 0; s[j] != NULL; j++) code = sdscatlen(code,s[j],strlen(s[j])); - luaL_loadbuffer(lua,code,sdslen(code),"@enable_strict_lua"); - lua_pcall(lua,0,0,0); - sdsfree(code); +static int luaProtectedTableError(lua_State *lua) { + int argc = lua_gettop(lua); + if (argc != 2) { + serverLog(LL_WARNING, "malicious code trying to call luaProtectedTableError with wrong arguments"); + luaL_error(lua, "Wrong number of arguments to luaProtectedTableError"); + } + if (!lua_isstring(lua, -1) && !lua_isnumber(lua, -1)) { + luaL_error(lua, "Second argument to luaProtectedTableError must be a string or number"); + } + const char *variable_name = lua_tostring(lua, -1); + luaL_error(lua, "Script attempted to access nonexistent global variable '%s'", variable_name); + return 0; } -/* Create a global protection function and put it to registry. - * This need to be called once in the lua_State lifetime. - * After called it is possible to use luaSetGlobalProtection - * to set global protection on a give table. +/* Set a special metatable on the table on the top of the stack. + * The metatable will raise an error if the user tries to fetch + * an un-existing value. * * The function assumes the Lua stack have a least enough * space to push 2 element, its up to the caller to verify - * this before calling this function. - * - * Notice, the difference between this and luaEnableGlobalsProtection - * is that luaEnableGlobalsProtection is enabling global protection - * on the current Lua globals. This registering a global protection - * function that later can be applied on any table. */ -void luaRegisterGlobalProtectionFunction(lua_State *lua) { - lua_pushstring(lua, REGISTRY_SET_GLOBALS_PROTECTION_NAME); - char *global_protection_func = "local dbg = debug\n" - "local globals_protection = function (t)\n" - " local mt = {}\n" - " setmetatable(t, mt)\n" - " mt.__newindex = function (t, n, v)\n" - " if dbg.getinfo(2) then\n" - " local w = dbg.getinfo(2, \"S\").what\n" - " if w ~= \"C\" then\n" - " error(\"Script attempted to create global variable '\"..tostring(n)..\"'\", 2)\n" - " end" - " end" - " rawset(t, n, v)\n" - " end\n" - " mt.__index = function (t, n)\n" - " if dbg.getinfo(2) and dbg.getinfo(2, \"S\").what ~= \"C\" then\n" - " error(\"Script attempted to access nonexistent global variable '\"..tostring(n)..\"'\", 2)\n" - " end\n" - " return rawget(t, n)\n" - " end\n" - "end\n" - "return globals_protection"; - int res = luaL_loadbuffer(lua, global_protection_func, strlen(global_protection_func), "@global_protection_def"); - serverAssert(res == 0); - res = lua_pcall(lua,0,1,0); - serverAssert(res == 0); - lua_settable(lua, LUA_REGISTRYINDEX); + * this before calling this function. */ +void luaSetErrorMetatable(lua_State *lua) { + lua_newtable(lua); /* push metatable */ + lua_pushcfunction(lua, luaProtectedTableError); /* push get error handler */ + lua_setfield(lua, -2, "__index"); + lua_setmetatable(lua, -2); } -/* Set global protection on a given table. - * The table need to be located on the top of the lua stack. - * After called, it will no longer be possible to set - * new items on the table. The function is not removing - * the table from the top of the stack! +static int luaNewIndexAllowList(lua_State *lua) { + int argc = lua_gettop(lua); + if (argc != 3) { + serverLog(LL_WARNING, "malicious code trying to call luaProtectedTableError with wrong arguments"); + luaL_error(lua, "Wrong number of arguments to luaNewIndexAllowList"); + } + if (!lua_istable(lua, -3)) { + luaL_error(lua, "first argument to luaNewIndexAllowList must be a table"); + } + if (!lua_isstring(lua, -2) && !lua_isnumber(lua, -2)) { + luaL_error(lua, "Second argument to luaNewIndexAllowList must be a string or number"); + } + const char *variable_name = lua_tostring(lua, -2); + /* check if the key is in our allow list */ + + char ***allow_l = allow_lists; + for (; *allow_l ; ++allow_l){ + char **c = *allow_l; + for (; *c ; ++c) { + if (strcmp(*c, variable_name) == 0) { + break; + } + } + if (*c) { + break; + } + } + if (!*allow_l) { + /* Search the value on the back list, if its there we know that it was removed + * on purpose and there is no need to print a warning. */ + char **c = deny_list; + for ( ; *c ; ++c) { + if (strcmp(*c, variable_name) == 0) { + break; + } + } + if (!*c) { + serverLog(LL_WARNING, "A key '%s' was added to Lua globals which is not on the globals allow list nor listed on the deny list.", variable_name); + } + } else { + lua_rawset(lua, -3); + } + return 0; +} + +/* Set a metatable with '__newindex' function that verify that + * the new index appears on our globals while list. * - * The function assumes the Lua stack have a least enough - * space to push 2 element, its up to the caller to verify - * this before calling this function. */ -void luaSetGlobalProtection(lua_State *lua) { - lua_pushstring(lua, REGISTRY_SET_GLOBALS_PROTECTION_NAME); - lua_gettable(lua, LUA_REGISTRYINDEX); - lua_pushvalue(lua, -2); - int res = lua_pcall(lua, 1, 0, 0); - serverAssert(res == 0); + * The metatable is set on the table which located on the top + * of the stack. + */ +void luaSetAllowListProtection(lua_State *lua) { + lua_newtable(lua); /* push metatable */ + lua_pushcfunction(lua, luaNewIndexAllowList); /* push get error handler */ + lua_setfield(lua, -2, "__newindex"); + lua_setmetatable(lua, -2); +} + +/* Set the readonly flag on the table located on the top of the stack + * and recursively call this function on each table located on the original + * table. Also, recursively call this function on the metatables.*/ +void luaSetTableProtectionRecursively(lua_State *lua) { + /* This protect us from a loop in case we already visited the table + * For example, globals has '_G' key which is pointing back to globals. */ + if (lua_isreadonlytable(lua, -1)) { + return; + } + + /* protect the current table */ + lua_enablereadonlytable(lua, -1, 1); + + lua_checkstack(lua, 2); + lua_pushnil(lua); /* Use nil to start iteration. */ + while (lua_next(lua,-2)) { + /* Stack now: table, key, value */ + if (lua_istable(lua, -1)) { + luaSetTableProtectionRecursively(lua); + } + lua_pop(lua, 1); + } + + /* protect the metatable if exists */ + if (lua_getmetatable(lua, -1)) { + luaSetTableProtectionRecursively(lua); + lua_pop(lua, 1); /* pop the metatable */ + } } void luaRegisterVersion(lua_State* lua) { @@ -1272,8 +1367,11 @@ void luaRegisterLogFunction(lua_State* lua) { } void luaRegisterRedisAPI(lua_State* lua) { + lua_pushvalue(lua, LUA_GLOBALSINDEX); + luaSetAllowListProtection(lua); + lua_pop(lua, 1); + luaLoadLibraries(lua); - luaRemoveUnsupportedFunctions(lua); lua_pushcfunction(lua,luaRedisPcall); lua_setglobal(lua, "pcall"); @@ -1504,9 +1602,19 @@ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t * EVAL received. */ luaCreateArray(lua,keys,nkeys); /* On eval, keys and arguments are globals. */ - if (run_ctx->flags & SCRIPT_EVAL_MODE) lua_setglobal(lua,"KEYS"); + if (run_ctx->flags & SCRIPT_EVAL_MODE){ + /* open global protection to set KEYS */ + lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0); + lua_setglobal(lua,"KEYS"); + lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1); + } luaCreateArray(lua,args,nargs); - if (run_ctx->flags & SCRIPT_EVAL_MODE) lua_setglobal(lua,"ARGV"); + if (run_ctx->flags & SCRIPT_EVAL_MODE){ + /* open global protection to set ARGV */ + lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0); + lua_setglobal(lua,"ARGV"); + lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1); + } /* At this point whether this script was never seen before or if it was * already defined, we can call it. |