Введение
Redis — это хранилище данных в оперативной памяти с открытым исходным кодом, широко используемое в качестве кэша, брокера сообщений и высокопроизводительной базы данных NoSQL. Оно предлагает богатые структуры данных, такие как строки, хеши, списки, наборы, сортированные наборы, битовые карты, HyperLogLogs и потоки, подкреплённые атомарными операциями и очень низкой задержкой. Персистентность данных обеспечивается с помощью снимков RDB и AOF, а высокая доступность обеспечивается репликацией, Sentinel и Cluster. Redis также поддерживает серверные скрипты на Lua для атомарного выполнения сложных операций. Его скорость, гибкость и развитая экосистема сделали его основным строительным блоком для современных систем, чувствительных к задержкам.
В октябре 2025 года Redis выпустила версию 8.2.2, релиз безопасности, исправляющий четыре уязвимости во встроенном движке Lua. Наиболее критичной из них является уязвимость Lua с использованием после освобождения, которая позволяет злоумышленникам выходить из песочницы скриптов и выполнять код на хосте, когда недоверенные пользователи могут запускать произвольный код Lua. В релизе также устранены проблемы переполнения целочисленных значений при распаковке, уязвимости межпользовательского выполнения скрипта и чтения за пределами выделенного буфера памяти в лексере Lua. В этой статье мы подробно рассмотрим каждую уязвимость. пройтись по патчами опишите практические шаги по повышению безопасности развертываний Redis.
Обзор уязвимостей
Прежде чем углубляться в код, вот краткий обзор того, что исправлено в версии 8.2.2 в движке Lua.
CVE-2025-46817: Целочисленное переполнение при распаковке (CVSS 9.8, Критическое)
Экстремальные аргументы функции unpack(tbl, i, j) могут привести к переполнению внутреннего счётчика возвращаемых значений. Этот сбойный счётчик может обойти проверки стека и привести к сбоям или повреждению памяти, что делает его полезным примитивом для эксплуатации.
CVE-2025-46818: Межпользовательское выполнение скрипта через общее состояние среды выполнения (CVSS 7.3, высокий уровень)
Недостаточный контроль над метатаблицами и API, связанными с окружением, позволял одному скрипту влиять на работу других. В некоторых случаях это означало, что пользователь с меньшими привилегиями мог влиять на выполнение в контексте другого пользователя, ослабляя изоляцию.
CVE-2025-46819: чтение за пределами выделенного буфера памяти Lua при разборе длинных строк (CVSS 7.1, High)
Нестабильная обработка разделителей длинных строк/длинных комментариев ([=[ … ]=] и т. д.) в лексере Lua может привести к выходу за пределы буфера. Это может привести к сбою Redis и, в крайних случаях, к раскрытию близлежащей памяти.
CVE-2025-49844: Lua use-after-free в парсере (CVSS 10.0, критическая ошибка)
Специально созданный скрипт Lua может манипулировать сборкой мусора и синтаксическим анализом таким образом, чтобы это приводило к использованию памяти после освобождения во встроенном движке Lua.
Далее мы рассмотрим каждый из них более подробно, с соответствующими фрагментами патча.
Исправления: подробный анализ уязвимостей
CVE-2025-46817: Целочисленное переполнение при распаковке
Эта проблема затрагивает функцию unpack(tbl, i, j) в Lua 5.1, встроенную в Redis. При экстремальных значениях i/j вычисление «сколько результатов возвращать» может привести к переполнению, появлению ложного счётчика и пропуску проверок стека. Это может привести к сбою или повреждению памяти и может быть использовано в качестве основы для RCE в неблагоприятных сценариях.
Патч для этой CVE можно найти здесь: fc9abc7
Анализ патча
Исправление делает обработку диапазонов unpack явной и безопасной.
- Подсчет количества переключился на безопасную беззнаковую математику
До патча:
статический int luaB_unpack (lua_State *L) {
int i, e, n;
luaL_checktype(L, 1, LUA_TTABLE);
я = luaL_optint(L, 2, 1);
e = luaL_opt(L, luaL_checkint, 3, luaL_getn(L, 1));
если (i > e) вернуть 0; /* пустой диапазон */
n = e – i + 1; /* возможно переполнение (знаковое) */
если (n <= 0 || !lua_checkstack(L, n))
return luaL_error(L, «слишком много результатов для распаковки»);
/* нажимаем n результатов… */
}
Здесь e – i + 1 выполняется в знаковом int. При экстремальных значениях (например, 0 и 2147483647) может произойти переполнение, а n <= 0 не является надёжной защитой.
После патча:
статический int luaB_unpack (lua_State *L) {
int i, e;
беззнаковое целое число n;
luaL_checktype(L, 1, LUA_TTABLE);
я = luaL_optint(L, 2, 1);
e = luaL_opt(L, luaL_checkint, 3, luaL_getn(L, 1));
если (i > e) вернуть 0; /* пустой диапазон */
n = (unsigned int)e – (unsigned int)i; /* элементы минус 1 */
если (n >= INT_MAX || !lua_checkstack(L, ++n))
return luaL_error(L, «слишком много результатов для распаковки»);
lua_rawgeti(L, 1, i); /* push arg[i] */
в то время как (i++ < e) {
lua_rawgeti(L, 1, i);
}
вернуть н;
}
Ключевые моменты:
- Для диапазона используется беззнаковое целое число, чтобы избежать отрицательных значений или переполнений.
- Обрабатывает n как «count – 1» и увеличивает на один раз перед проверкой стека.
- Обеспечивает соблюдение жесткая верхняя граница (n >= INT_MAX → ошибка).
- Гарантирует, что в стеке достаточно места для всех результатов до толкать что-либо.
- Индексация массива сделана явной
Соответствующее ужесточение в табличном коде:
После патча
если (1 <= ключ && ключ <= t->sizearray)
возврат &t->массив[ключ-1];
Вместо того чтобы полагаться на трюк с беззнаковым приведением типа, он использует четкую проверку границ, что позволяет избежать странного поведения для огромных или отрицательных индексов.
В совокупности эти изменения означают, что экстремальные диапазоны распаковки больше не приводят к перерасчёту счётчиков или небезопасным push-запросам. Вызовы слишком большого размера теперь завершаются с явной ошибкой «слишком много результатов для распаковки», а не рискуют привести к сбою или повреждению памяти.
CVE-2025-46819: чтение за пределами выделенного буфера памяти Lua при разборе длинных строк
Эта проблема кроется в лексере Lua, а именно в том, как он обрабатывает длинные строки и длинные комментарии, такие как [=[ … ]=]. В уязвимых версиях некоторые некорректные или экстремальные шаблоны разделителей могут вывести лексер за пределы входного буфера. Это может привести к сбою Redis (DoS-атака), а в некоторых крайних случаях — к раскрытию байтов из близлежащей памяти.
Патч для этой CVE можно найти здесь: 3a1624d
Анализ патча
Благодаря этому решению обработка длинных струн становится более строгой и менее хрупкой.
Анализ разделителей сделан однозначным
Вспомогательный метод, который анализирует последовательности типа [===[ и ]===], обновлен для использования более безопасного типа и возврата понятных, непротиворечивых значений:
После патча
статический размер_t skip_sep(LexState *ls) {
size_t количество = 0;
int s = ls->current; /* '[' или ']' */
lua_assert(s == '[' || s == ']');
save_and_next(ls); /* использовать его */
while (ls->current == '=') { /* количество '=' */
сохранить_и_следующий(ls);
count ++;
}
if (ls->current == s) /* совпало второе '[' или ']' */
return count + 2; /* допустимый длинный разделитель */
иначе если (количество == 0)
return 1; /* не длинная строка/комментарий */
еще
return 0; /* некорректно */
}
- Допустимые разделители длинных строк теперь возвращают четко определенную длину (>= 2).
- Простой [ отступает чисто (1).
- Неверно сформированные шаблоны [==… возвращают 0 и рассматриваются как ошибки.
Это устраняет пробел, в котором странные или неполные шаблоны могли бы запутать лексер и заставить его читать за пределами буфера.
Исправлены смещения для нарезки тела.
При извлечении фактического содержимого строки патч выравнивает математику среза с результатом skip_sep:
После патча:
если (seminfo) {
seminfo->ts = luaX_newstring(
лс,
luaZ_buffer(ls->buff) + sep,
luaZ_buffen(ls->buff) – 2 * сентябрь
);
}
Раньше смещения не соответствовали семантике разделителей, что могло приводить к неявному отклонению от N. Теперь начало и длина определяются непосредственно из sep, поэтому лексер остаётся в рамках допустимых границ.
Типы соответствуют тому, что на самом деле делает код
Счетчики и индексы, задействованные в этой логике, теперь используют соответствующие типы unsigned/size вместо перегруженных знаковых целых чисел, что снижает риск появления отрицательных значений или проникновения в арифметику указателей.
Всё это означает, что даже огромные или намеренно сломанные последовательности [=…=[ … ]=…=] теперь Lua либо корректно парсит их, либо отклоняет с обычной ошибкой. Больше нет пути, по которому вредоносный скрипт мог бы подтолкнуть лексер к просмотру за пределами буфера.
CVE-2025-46818: Скрипт Lua может запускаться в контексте другого пользователя
Этот выпуск посвящен сохранение Lua-скриптов в рамках пользователя и среды, к которой они принадлежатВ уязвимых версиях общие элементы среды выполнения, такие как метатаблицы основных типов и устаревшие API среды, были настолько свободны, что один скрипт мог влиять на работу других. В некоторых развёртываниях это могло привести к размыванию изоляции между пользователями.
Патч для этой CVE можно найти здесь: 45eac02
Анализ патча
Это исправление решает две основные задачи: защищает основные метатаблицы и Gates рискованные устаревшие API.
- Метатаблицы основного типа защищены
Раньше основные типы Lua, доступные скриптам (строки, числа, логические значения и т. д.), не имели строгого барьера, предотвращающего изменение метатаблиц из пользовательских скриптов. Это означало, что изменение общей метатаблицы могло повлиять на другой код, работающий в том же движке.
Исправленный код явно усиливает защиту этих метатаблиц во время настройки среды Lua. В упрощенном виде:
Исправленный код: пометить примитивные метатаблицы как защищенные
static void luaProtectPrimitiveMetatables(lua_State *L) {
const int типы[] = {LUA_TSTRING, LUA_TNUMBER, LUA_TBOOLEAN, LUA_TNIL};
для (size_t i = 0; i < sizeof(types)/sizeof(types[0]); i++) {
luaL_getmetatable(L, lua_typename(L, types[i]));
если (!lua_isnil(L, -1)) {
lua_pushliteral(L, «защищено»);
lua_setfield(L, -2, "__metatable"); /* блокировка метатаблицы */
}
lua_pop(L, 1);
}
}
Эта __метатабличная защита означает, что пользовательские скрипты по-прежнему могут увидели Эти типы, однако, не могут заменить или переписать свои метатаблицы глобально. Любая попытка сделать это завершается чистой ошибкой, а не приводит к незаметному изменению поведения для всех остальных.
- API устаревших сред явно включены
Старые API Lua, которые могли манипулировать окружением, теперь по умолчанию отключено и становятся доступными только тогда, когда оператор явно включает их.
Концептуально изменение выглядит так:
До патча: устаревшие API всегда доступны в окружении скрипта
статический const luaL_Reg redis_compat_funcs[] = {
{"getfenv", luaB_getfenv},
{"setfenv", luaB_setfenv},
{«newproxy», luaB_newproxy},
{НУЛЬ, НУЛЬ}
};
scriptCreateEnv(…) {
luaL_register(L, "_G", redis_compat_funcs);
}
После патча: регистрировать только при установленном lua-enable-deprecated-api
scriptCreateEnv(…) {
если (server.lua_enable_deprecated_api) {
luaL_register(L, "_G", redis_compat_funcs);
}
}
Если lua-enable-deprecated-api не включен, эти функции просто отсутствуют в среде скрипта, что исключает мощный набор рычагов для кросс-контекстного манипулирования.
В совокупности эти изменения обеспечивают:
- Скрипты не могут незаметно исправить фундаментальное поведение всех остальных скриптов путем переписывания основных метатаблиц.
- Примитивы манипуляции средой Важно доступны, если оператор намеренно включит их.
Это делает среду выполнения Lua более предсказуемой и позволяет держать каждый скрипт гораздо ближе к предполагаемым привилегиям и контексту, что важно для любого общего или многопользовательского развертывания Redis.
CVE-2025-49844: Lua use-after-free в парсере
Эта ошибка связана с тем, как Redis интегрирует парсер Lua. При определённых условиях специально созданный скрипт Lua может запустить сборку мусора в самый неподходящий момент, и Redis будет читать указатель на уже освобождённую память.
Патч для этой CVE можно найти здесь: д5728кб5795
Анализ патча
Ключевое изменение заключается в том, что Redis теперь сохраняет правильную ссылку на объекты Lua, которые использует анализатор (например, имя фрагмента) в стеке Lua во время анализа, поэтому сборщик мусора видит их как активные и не может освободить или переместить их раньше времени.

Раньше фрагмент «имя» создавался встроенным и передавался напрямую в лексер:
luaX_setinput(L, &lexstate, z, luaS_new(L, name));
Поскольку это значение не было четко закреплено (например, в стеке Lua), неудачное чередование GC или defrag с агрессивным выполнением скриптов теоретически могло бы оставить анализатор с указателем на что-то, что было перемещено или освобождено.
Исправленный код делает это владение явным:
TString *tname = luaS_new(L, name);
setsvalue2s(L, L->top, tname); /* сохраняем имя в стеке */
incr_top(L);
luaX_setinput(L, &lexstate, z, tname); /* безопасно использовать его для лексического анализа/парсинга */
/* … разобрать фрагмент … */
–L->top; /* по завершении удалите ссылку */
Сохраняя ссылку на это состояние для всего анализа, сборщик мусора и дефрагментатор теперь считают его активным и не могут освободить или переместить его в процессе выполнения. Это фактически устраняет узкое временное окно, которое делало возможным использование после освобождения.
Рискованные сценарии развертывания
Эти проблемы особенно актуальны в условиях, когда:
- Приложения используют скрипты Lua в Redis.
- Один и тот же кластер Redis используется всеми командами или арендаторами.
- Доступ к Redis можно осуществить напрямую или через ваше приложение — из ненадежного или контролируемого пользователем источника ввода.
Рекомендуемые действия
1. Сначала пропатчить
- Обновитесь до исправленной сборки Redis 8.2.2.
- Относитесь к этому как к высокоприоритетному везде, где ненадежный или полунадежный код может запускать Lua (например, через EVAL / SCRIPT LOAD) или где кластеры Redis используются совместно несколькими командами/арендаторами.
2. Заблокируйте скрипты Lua
- Используйте списки контроля доступа (ACL), чтобы ограничить выполнение скриптов Lua только доверенными службами.
- Храните Redis в частных сетях, без прямого доступа к Интернету.
- Оставьте lua-enable-deprecated-api отключенным, если у вас нет явной, проверенной необходимости в этом.
3. Сегментация чувствительных рабочих нагрузок
- Запускайте ненадежный или управляемый клиентом Lua на отдельных экземплярах Redis.
- Не смешивайте скрипты, контролируемые арендаторами, с кластерами, содержащими критически важные или конфиденциальные данные.
4. Используйте средства контроля безопасности для повышения прозрачности (не в качестве замены исправлениям)
- Настройте IPS/IDS/WAF для обнаружения и блокирования прямого доступа к Redis и подозрительного использования протокола Redis.
- Используйте EDR/AV/защиту времени выполнения на хостах Redis для обнаружения поведения, похожего на эксплойт
Ссылки:
- https://github.com/redis/redis/commits/8.2.2/
- https://nvd.nist.gov/vuln/detail/CVE-2025-46817
- https://nvd.nist.gov/vuln/detail/CVE-2025-46818
- https://nvd.nist.gov/vuln/detail/CVE-2025-46819
- https://nvd.nist.gov/vuln/detail/CVE-2025-49844
Авторы:
Винай Кумар
Адрип Мукерджи
Нандини Сет
Суварнджит Джагтап



