File CVE-2025-46818.patch of Package redis7.41030

From dccb672d838f05c940f040c27b74fde6fb47b2a7 Mon Sep 17 00:00:00 2001
From: Ozan Tezcan <ozantezcan@gmail.com>
Date: Mon, 23 Jun 2025 12:10:12 +0300
Subject: [PATCH] Lua script can be executed in the context of another user
 (CVE-2025-46818)

---
 src/config.c             |  1 +
 src/eval.c               |  2 ++
 src/function_lua.c       |  2 ++
 src/script_lua.c         | 59 +++++++++++++++++++++++++++++----
 src/script_lua.h         |  1 +
 src/server.h             |  1 +
 tests/unit/scripting.tcl | 70 ++++++++++++++++++++++++++++++++++++++++
 7 files changed, 129 insertions(+), 7 deletions(-)

Index: b/src/config.c
===================================================================
--- a/src/config.c
+++ b/src/config.c
@@ -3011,6 +3011,7 @@ standardConfig static_configs[] = {
     createBoolConfig("latency-tracking", NULL, MODIFIABLE_CONFIG, server.latency_tracking_enabled, 1, NULL, NULL),
     createBoolConfig("aof-disable-auto-gc", NULL, MODIFIABLE_CONFIG, server.aof_disable_auto_gc, 0, NULL, updateAofAutoGCEnabled),
     createBoolConfig("replica-ignore-disk-write-errors", NULL, MODIFIABLE_CONFIG, server.repl_ignore_disk_write_error, 0, NULL, NULL),
+    createBoolConfig("lua-enable-deprecated-api", NULL, IMMUTABLE_CONFIG | HIDDEN_CONFIG, server.lua_enable_deprecated_api, 0, NULL, NULL),
 
     /* String Configs */
     createStringConfig("aclfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.acl_filename, "", NULL, NULL),
Index: b/src/eval.c
===================================================================
--- a/src/eval.c
+++ b/src/eval.c
@@ -261,6 +261,8 @@ void scriptingInit(int setup) {
     /* Recursively lock all tables that can be reached from the global table */
     luaSetTableProtectionRecursively(lua);
     lua_pop(lua, 1);
+    /* Set metatables of basic types (string, number, nil etc.) readonly. */
+    luaSetTableProtectionForBasicTypes(lua);
 
     lctx.lua = lua;
 }
Index: b/src/function_lua.c
===================================================================
--- a/src/function_lua.c
+++ b/src/function_lua.c
@@ -489,6 +489,8 @@ int luaEngineInitEngine() {
     lua_enablereadonlytable(lua_engine_ctx->lua, -1, 1); /* protect the new global table */
     lua_replace(lua_engine_ctx->lua, LUA_GLOBALSINDEX); /* set new global table as the new globals */
 
+    /* Set metatables of basic types (string, number, nil etc.) readonly. */
+    luaSetTableProtectionForBasicTypes(lua_engine_ctx->lua);
 
     engine *lua_engine = zmalloc(sizeof(*lua_engine));
     *lua_engine = (engine) {
Index: b/src/script_lua.c
===================================================================
--- a/src/script_lua.c
+++ b/src/script_lua.c
@@ -65,7 +65,6 @@ static char *redis_api_allow_list[] = {
 static char *lua_builtins_allow_list[] = {
     "xpcall",
     "tostring",
-    "getfenv",
     "setmetatable",
     "next",
     "assert",
@@ -86,15 +85,16 @@ static char *lua_builtins_allow_list[] =
     "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[] = {
+/* Lua builtins which are deprecated for sandboxing concerns */
+static char *lua_builtins_deprecated[] = {
     "newproxy",
+    "setfenv",
+    "getfenv",
     NULL,
 };
 
@@ -116,7 +116,6 @@ 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,
 };
@@ -1323,7 +1322,22 @@ static int luaNewIndexAllowList(lua_Stat
             break;
         }
     }
-    if (!*allow_l) {
+
+    int allowed = (*allow_l != NULL);
+    /* If not explicitly allowed, check if it's a deprecated function. If so,
+     * allow it only if 'lua_enable_deprecated_api' config is enabled. */
+    int deprecated = 0;
+    if (!allowed) {
+        char **c = lua_builtins_deprecated;
+        for (; *c; ++c) {
+            if (strcmp(*c, variable_name) == 0) {
+                deprecated = 1;
+                allowed = server.lua_enable_deprecated_api ? 1 : 0;
+                break;
+            }
+        }
+    }
+    if (!allowed) {
         /* 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;
@@ -1332,7 +1346,7 @@ static int luaNewIndexAllowList(lua_Stat
                 break;
             }
         }
-        if (!*c) {
+        if (!*c && !deprecated) {
             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 {
@@ -1384,6 +1398,37 @@ void luaSetTableProtectionRecursively(lu
     }
 }
 
+/* Set the readonly flag on the metatable of basic types (string, nil etc.) */
+void luaSetTableProtectionForBasicTypes(lua_State *lua) {
+    static const int types[] = {
+        LUA_TSTRING,
+        LUA_TNUMBER,
+        LUA_TBOOLEAN,
+        LUA_TNIL,
+        LUA_TFUNCTION,
+        LUA_TTHREAD,
+        LUA_TLIGHTUSERDATA
+    };
+
+    for (size_t i = 0; i < sizeof(types) / sizeof(types[0]); i++) {
+        /* Push a dummy value of the type to get its metatable */
+        switch (types[i]) {
+            case LUA_TSTRING: lua_pushstring(lua, ""); break;
+            case LUA_TNUMBER: lua_pushnumber(lua, 0); break;
+            case LUA_TBOOLEAN: lua_pushboolean(lua, 0); break;
+            case LUA_TNIL: lua_pushnil(lua); break;
+            case LUA_TFUNCTION: lua_pushcfunction(lua, NULL); break;
+            case LUA_TTHREAD: lua_newthread(lua); break;
+            case LUA_TLIGHTUSERDATA: lua_pushlightuserdata(lua, (void*)lua); break;
+        }
+        if (lua_getmetatable(lua, -1)) {
+            luaSetTableProtectionRecursively(lua);
+            lua_pop(lua, 1); /* pop metatable */
+        }
+        lua_pop(lua, 1); /* pop dummy value */
+    }
+}
+
 void luaRegisterVersion(lua_State* lua) {
     lua_pushstring(lua,"REDIS_VERSION_NUM");
     lua_pushnumber(lua,REDIS_VERSION_NUM);
Index: b/src/script_lua.h
===================================================================
--- a/src/script_lua.h
+++ b/src/script_lua.h
@@ -71,6 +71,7 @@ void luaRegisterGlobalProtectionFunction
 void luaSetErrorMetatable(lua_State *lua);
 void luaSetAllowListProtection(lua_State *lua);
 void luaSetTableProtectionRecursively(lua_State *lua);
+void luaSetTableProtectionForBasicTypes(lua_State *lua);
 void luaRegisterLogFunction(lua_State* lua);
 void luaRegisterVersion(lua_State* lua);
 void luaPushErrorBuff(lua_State *lua, sds err_buff);
Index: b/src/server.h
===================================================================
--- a/src/server.h
+++ b/src/server.h
@@ -1897,6 +1897,7 @@ struct redisServer {
     mstime_t busy_reply_threshold;  /* Script / module timeout in milliseconds */
     int pre_command_oom_state;         /* OOM before command (script?) was started */
     int script_disable_deny_script;    /* Allow running commands marked "no-script" inside a script. */
+    int lua_enable_deprecated_api;     /* Config to enable deprecated api */
     /* Lazy free */
     int lazyfree_lazy_eviction;
     int lazyfree_lazy_expire;
Index: b/tests/unit/scripting.tcl
===================================================================
--- a/tests/unit/scripting.tcl
+++ b/tests/unit/scripting.tcl
@@ -982,6 +982,27 @@ start_server {tags {"scripting"}} {
         set _ $e
     } {*Attempt to modify a readonly table*}
 
+    test "Try trick readonly table on basic types metatable" {
+        # Run the following scripts for basic types. Either getmetatable()
+        # should return nil or the metatable must be readonly.
+        set scripts {
+            {getmetatable(nil).__index = function() return 1 end}
+            {getmetatable('').__index = function() return 1 end}
+            {getmetatable(123.222).__index = function() return 1 end}
+            {getmetatable(true).__index = function() return 1 end}
+            {getmetatable(function() return 1 end).__index = function() return 1 end}
+            {getmetatable(coroutine.create(function() return 1 end)).__index = function() return 1 end}
+        }
+
+        foreach code $scripts {
+            catch {run_script $code 0} e
+            assert {
+                [string match "*attempt to index a nil value script*" $e] ||
+                [string match "*Attempt to modify a readonly table*" $e]
+            }
+        }
+    }
+
     test "Test loadfile are not available" {
         catch {
             run_script {
@@ -1010,6 +1031,55 @@ start_server {tags {"scripting"}} {
     } {*Script attempted to access nonexistent global variable 'print'*}
 }
 
+# Start a new server to test lua-enable-deprecated-api config
+foreach enabled {no yes} {
+start_server [subst {tags {"scripting external:skip"} overrides {lua-enable-deprecated-api $enabled}}] {
+    test "Test setfenv availability lua-enable-deprecated-api=$enabled" {
+        catch {
+            run_script {
+                local f = function() return 1 end
+                setfenv(f, {})
+                return 0
+            } 0
+        } e
+        if {$enabled} {
+            assert_equal $e 0
+        } else {
+            assert_match {*Script attempted to access nonexistent global variable 'setfenv'*} $e
+        }
+    }
+
+    test "Test getfenv availability lua-enable-deprecated-api=$enabled" {
+        catch {
+            run_script {
+                local f = function() return 1 end
+                getfenv(f)
+                return 0
+            } 0
+        } e
+        if {$enabled} {
+            assert_equal $e 0
+        } else {
+            assert_match {*Script attempted to access nonexistent global variable 'getfenv'*} $e
+        }
+    }
+
+    test "Test newproxy availability lua-enable-deprecated-api=$enabled" {
+        catch {
+            run_script {
+                getmetatable(newproxy(true)).__gc = function() return 1 end
+                return 0
+            } 0
+        } e
+        if {$enabled} {
+            assert_equal $e 0
+        } else {
+            assert_match {*Script attempted to access nonexistent global variable 'newproxy'*} $e
+        }
+    }
+}
+}
+
 # Start a new server since the last test in this stanza will kill the
 # instance at all.
 start_server {tags {"scripting"}} {
openSUSE Build Service is sponsored by