Redis Lua脚本安全核心是禁止动态代码生成:用户输入只能作为ARGV/KEYS传入并严格校验,禁用loadstring、字符串拼接命令名、不信任redis.call参数,数值参数须转number并设上下界,权限等强逻辑不得下沉至Lua,上线前必须SCRIPT LOAD预注册、参数白名单校验及错误响应收敛。

Redis Lua脚本里不能直接拼接ARGV或KEYS
Redis 的 Lua 环境是沙箱化的,但不等于安全——只要用 loadstring、load 或字符串拼接把用户输入塞进脚本体,就等于开了后门。真实场景中,很多人误以为“只传给 eval 的 ARGV 是安全的”,其实不然:如果脚本里写了 loadstring("return "..ARGV[1]),攻击者传 "1; os.execute('rm -rf /')"(虽然 Redis 会禁掉 os,但绕过方式很多),或者更现实的:"' or 1==1 or '" 来干扰逻辑判断。
正确做法只有一条:所有用户输入必须严格作为 ARGV 或 KEYS 传入,且在 Lua 脚本里只作值比较、拼接(用 table.concat)、或传给 Redis 命令(如 redis.call("GET", ARGV[1])),绝不参与代码生成。
- 禁止在脚本里调用
loadstring、load、assert(load(...)) - 禁止用
..拼接出新命令名,比如redis.call(cmd_name .. "_ex", ...) - 若需动态 key 名,应由客户端预计算好再传入
KEYS或ARGV,而非在 Lua 里字符串拼接
用 redis.call() 和 redis.pcall() 时别信 ARGV 是“干净”的
redis.call() 不会帮你过滤参数——它只是把 ARGV[1] 当成 key 名或 value 原样发给底层命令。比如你写 redis.call("SET", "user:"..ARGV[1], ARGV[2]),表面看只是拼了前缀,但如果 ARGV[1] 是 "123; DEL user:admin",那 key 名就变成 "user:123; DEL user:admin",虽不会执行语句,但可能撞上业务里用 SCAN 匹配 key 的逻辑,造成越权读取。
更隐蔽的问题是类型混淆:Redis 命令对参数类型敏感,INCR 要数字,ARGV[1] 若含非数字字符,redis.call("INCR", ARGV[1]) 直接报错 (error) ERR value is not an integer or out of range,而错误本身可能泄露信息(比如通过响应时间/错误码判断 key 是否存在)。
- 所有传给
redis.call()的参数,先用tonumber()或正则校验格式,失败则return error("invalid arg") - 避免在脚本里做“尝试性调用”,比如用
pcall包一层然后根据成功与否分支逻辑——这容易被时序攻击利用 - 如果必须用通配符匹配,改用服务端已有的命令(如
SCAN MATCH user:*),而不是在 Lua 里遍历 +string.match()
别把 Lua 脚本当通用逻辑层,尤其涉及权限或数据边界
有人用 Lua 实现“检查用户是否有某资源权限”,脚本里读多个 key、比对字段、返回布尔值。问题在于:Lua 脚本执行是原子的,但权限判定不该依赖原子性——它该是可审计、可拦截、可打日志的。一旦逻辑写死在 Lua 里,改个判断条件就得重发脚本 SHA1,上线风险高;更麻烦的是,你没法在中间件层统一鉴权,Redis 就成了权限黑洞。
另一个典型坑是分页:用 LRANGE + ARGV 做 offset/limit,看似简单,但没做 math.max(0, math.floor(tonumber(ARGV[1]))) 校验,攻击者传负数或超大数,可能触发 OOM 或慢查询。
- 权限、计费、风控等强业务逻辑,坚决不下沉到 Lua;Redis 只做“存”和“取”,不做“判”
- 数值类参数(offset、limit、score、timeout)必须显式转
number并设上下界,例如local limit = math.min(100, math.max(1, tonumber(ARGV[2]) or 0)) - 涉及多 key 操作,优先用原生命令(如
MGET、DEL)而非在 Lua 里循环redis.call("GET", ...),后者性能差还容易超时
上线前必须做三件事:SHA1 预注册、参数白名单、错误响应收敛
Redis 6.0+ 支持 SCRIPT LOAD 预加载脚本并用 EVALSHA 执行,这不只是为性能——它让你能提前审查脚本内容,且运行时只传 SHA1,不暴露逻辑。但很多人跳过这步,直接 EVAL 字符串,等于每次都在动态注入。
同时,不要指望靠“前端校验”或“API 层过滤”保 Lua 安全。攻击者可以绕过所有中间层,直连 Redis 发 EVAL。所以关键动作得落在 Redis 侧或紧邻它的代理层:
- 所有 Lua 脚本上线前走
SCRIPT LOAD,应用只允许用EVALSHA,禁用EVAL命令(可通过rename-command EVAL ""在配置里关掉) - 对每个脚本的
ARGV个数、每个参数的格式(如是否为 UUID、是否为纯数字、长度范围)建白名单,在脚本开头硬校验,不满足立刻return error("bad arg") - 统一错误响应:不要让不同错误路径返回不同提示(比如 “key not found” vs “invalid type”),全统一成
error("operation failed"),避免信息泄露
最麻烦的点往往不在脚本本身,而在“谁有权限执行哪个 SHA1”。Redis 没内置脚本级权限控制,得靠外部代理或连接池做路由隔离——这点很容易被忽略,等真出了问题才意识到,脚本是共享的,但数据不是。

















