Redis从2.6版本开始引入对Lua脚本的支持,开发者可以通过EVAL命令把一段Lua脚本发送到服务端执行,从而把多条Redis命令打包成一个原子操作。不过Redis并没有把一个完整的Lua解释器直接暴露给用户,而是构建了一个经过严格裁剪的沙箱环境。理解这个沙箱的限制机制、可用能力以及历史上出现过的逃逸风险,对安全加固和正确使用脚本功能都非常重要。

Redis Lua沙箱的构建原理
Redis在嵌入Lua解释器时做了一系列主动的裁剪工作。首先,它并没有链接Lua官方发行版中的全部标准库,而是只加载了被认为安全的子集,包括string、table、math、bit、cjson和结构化输出相关的cmsgpack等库。其次,即便某些库被加载了,其中的危险函数也会被逐一移除。
最典型的处理方式是直接把loadfile和dofile设为nil,这样脚本就无法从磁盘加载任意Lua文件。print函数被替换成了一个发送数据到Redis日志的版本,而不是输出到标准输出。os库中只保留了clock、time、diff等少数时间相关函数,os.execute、os.getenv、os.remove统统被剔除。io库和require函数则完全没有被加载,这就切断了脚本读写文件系统和动态加载模块的途径。
除了函数层面的裁剪,Redis还在运行层面做了约束。脚本执行期间Redis是单线程阻塞的,因此脚本里没有真正的并发能力,也不会有操作系统层面的异步回调入口。沙箱内的代码只能通过redis.call和redis.pcall与外界交互,这两个函数是沙箱对外的唯一通道,它们的参数和返回值都会经过Redis的类型检查与转换。
沙箱内可用的能力清单
在沙箱里,脚本可以自由使用Lua语言本身的全部语法特性,包括闭包、协程、元表等。标准库方面,string库的格式化、匹配,table库的排序、拼接,math库的数学运算都是可用的。此外Redis还注入了几个专属库,比如cjson用于JSON序列化与反序列化,cmsgpack用于MessagePack编码,bit用于位运算,redis.sha1hex用于计算哈希。
-- 沙箱内的合法脚本示例
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key) or '0')
if current + 1 > limit then
return 0
end
redis.call('INCRBY', key, 1)
redis.call('EXPIRE', key, ARGV[2])
return 1
上面这段限流脚本展示了沙箱内的典型用法:读取键值、做数值比较、再写回结果。整个过程不涉及任何文件或进程操作,纯粹在Redis数据层面完成,这正是沙箱设计所希望达到的效果。
需要注意的是,沙箱虽然限制了对外能力,却放大了对内的破坏力。脚本执行期间整个Redis服务是阻塞的,一个写了死循环的脚本可以直接把生产实例卡死,直到SCRIPT KILL介入或者实例崩溃。这也是后来Redis引入busy-reply-threshold参数和SCRIPT KILL机制的原因。如果脚本已经执行过写操作,则连SCRIPT KILL都无能为力,只能等待SHUTDOWN NOSAVE。
历史上的沙箱逃逸与绕过风险
沙箱并非固若金汤,历史上多次出现过逃逸漏洞。比较著名的是CVE-2015-4335,问题出在Lua的C API使用上。攻击者可以通过精心构造的脚本,利用错误处理路径中对象未正确清理的缺陷,间接访问到沙箱外的Lua全局环境,进而恢复被禁用的os.execute等函数,最终在服务器上执行任意命令。这类漏洞说明沙箱的安全性高度依赖实现细节,裁剪函数只是第一道防线。
另一个思路是利用package或require的残留。早期某些Redis版本或第三方魔改版本中,如果package.loaded表仍然可以访问,攻击者就有机会重新加载被禁用的模块。虽然官方版本早已封堵这类入口,但在审计自编译或打了补丁的Redis时,仍然值得检查package是否真的为nil。
除了真正的沙箱逃逸,还有一类逻辑层面的绕过。攻击者如果不能控制Redis实例本身,而是通过未授权访问的Redis写入脚本,那么即便沙箱限制再严格,攻击者也可以利用脚本本身的计算能力做资源耗尽攻击,比如构造海量字符串拼接造成内存暴涨。这类攻击不需要逃出沙箱,却同样能造成服务不可用。
如何加固Redis的脚本执行环境
第一道防线永远是网络层面的隔离。Redis默认不开启密码认证,脚本沙箱再安全也无法弥补一个暴露在公网的裸奔实例。建议开启requirepass或使用ACL用户体系,并配合防火墙把Redis端口限制在内网范围。
第二是及时升级版本。上述逃逸漏洞在较新版本中都已修复,长期停留在老版本会让自己暴露在已知的攻击面之下。如果业务允许,也可以考虑用FUNCTION命令配合的Redis Function替代传统EVAL脚本,新机制在权限管理上更加清晰。
第三是善用配置项进行纵深防御。通过rename-command把EVAL、EVALSHA、SCRIPT等高危命令改名甚至禁用,可以显著降低攻击者利用脚本接口的可能性。同时合理设置busy-reply-threshold,确保脚本卡死时哨兵或客户端能及时发现异常。下面的配置展示了常见的加固组合:
# redis.conf 加固示例 requirepass YourStrongPasswordHere rename-command EVAL "" rename-command EVALSHA "e9a1c3f7d2b4" rename-command CONFIG "b8f2a6d4c1e3" busy-reply-threshold 5000
最后要建立监控习惯。关注slowlog中的脚本执行记录,观察脚本执行耗时的变化趋势,一旦发现异常长的脚本,及时排查是否被注入了恶意代码。沙箱解决的是能力限制问题,而整体安全还需要认证、网络、版本、监控多层措施共同兜底。
总结
Redis的Lua沙箱通过裁剪标准库、移除危险函数、限制对外通道这三种手段,构建了一个以数据操作为核心目的的受限执行环境。沙箱内保留了足够的能力去实现原子业务逻辑,同时隔离了文件系统和命令执行。但历史漏洞证明沙箱不是绝对安全的边界,运维侧必须配合认证、网络隔离、命令重命名和版本升级等措施,才能把脚本功能的价值发挥出来,同时把风险控制在可接受的范围内。
Redis Lua沙箱Redis脚本安全EVAL命令修改时间:2026-09-01 18:38:37