1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
|
From 45eac0262028c771b6f5307372814b75f49f7a9e 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)
--- redis-8.0.2.orig/src/config.c
+++ redis-8.0.2/src/config.c
@@ -3114,6 +3114,7 @@ standardConfig static_configs[] = {
createBoolConfig("aof-disable-auto-gc", NULL, MODIFIABLE_CONFIG | HIDDEN_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("hide-user-data-from-log", NULL, MODIFIABLE_CONFIG, server.hide_user_data_from_log, 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),
--- redis-8.0.2.orig/src/eval.c
+++ redis-8.0.2/src/eval.c
@@ -252,6 +252,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;
}
--- redis-8.0.2.orig/src/function_lua.c
+++ redis-8.0.2/src/function_lua.c
@@ -495,6 +495,8 @@ int luaEngineInitEngine(void) {
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) {
--- redis-8.0.2.orig/src/script_lua.c
+++ redis-8.0.2/src/script_lua.c
@@ -47,7 +47,6 @@ static char *redis_api_allow_list[] = {
static char *lua_builtins_allow_list[] = {
"xpcall",
"tostring",
- "getfenv",
"setmetatable",
"next",
"assert",
@@ -68,15 +67,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,
};
@@ -98,7 +98,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,
};
@@ -1301,7 +1300,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;
@@ -1310,7 +1324,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 {
@@ -1362,6 +1376,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);
--- redis-8.0.2.orig/src/script_lua.h
+++ redis-8.0.2/src/script_lua.h
@@ -51,6 +51,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);
--- redis-8.0.2.orig/src/server.h
+++ redis-8.0.2/src/server.h
@@ -2194,6 +2194,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 "noscript" inside a script. */
+ int lua_enable_deprecated_api; /* Config to enable deprecated api */
/* Lazy free */
int lazyfree_lazy_eviction;
int lazyfree_lazy_expire;
--- redis-8.0.2.orig/tests/unit/scripting.tcl
+++ redis-8.0.2/tests/unit/scripting.tcl
@@ -1078,6 +1078,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 {
@@ -1106,6 +1127,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"}} {
|