Redis 允许客户端提交一段 Lua 脚本在服务端运行。那它能碰到什么?能不能读文件、执行系统命令、改全局环境?ACL 对脚本内部有没有效?这篇只做一件事:在本机的 8.10.2 上把沙箱的边界逐条试出来,并给出限制脚本时容易漏掉的地方。
全局变量: __redis__err__handler _G _VERSION ARGV assert bit cjson cmsgpack collectgarbage coroutine error gcinfo getmetatable ipairs KEYS load loadstring math next os pairs pcall rawequal rawget rawset redis select setmetatable string struct table tonumber tostring type unpack xpcall
库(type):
os table
io 不存在(读取报错:nonexistent global 'io')
package 不存在(读取报错:nonexistent global 'package')
debug 不存在(读取报错:nonexistent global 'debug')
string table
table table
math table
cjson table
cmsgpack table
struct table
bit table
redis table
函数(type):
require 不存在(读取报错:nonexistent global 'require')
dofile 不存在(读取报错:nonexistent global 'dofile')
loadfile 不存在(读取报错:nonexistent global 'loadfile')
loadstring function
load function
print 不存在(读取报错:nonexistent global 'print')
setmetatable function
getmetatable function
rawset function
collectgarbage function
pcall function
newproxy 不存在(读取报错:nonexistent global 'newproxy')
loadstring("return 1+1")() → 2 | loadstring("x = 1")() → ERR [string "x = 1"]:1: Attempt to modify a readonly table script: afbff2ee5a9bb020f9918fdbbf2a281dc3fec921, on x = 1:1.
os 里有: clock
os.execute 的类型: nil
redis 表里有: acl_check_cmd breakpoint call debug error_reply log LOG_DEBUG LOG_NOTICE LOG_VERBOSE LOG_WARNING pcall REDIS_VERSION REDIS_VERSION_NUM REPL_ALL REPL_AOF REPL_NONE REPL_REPLICA REPL_SLAVE replicate_commands set_repl setresp sha1hex status_reply沙箱里没有文件、进程、模块加载的入口:io、package、require、dofile、loadfile、print 读取时直接报 nonexistent global;os 只剩 clock。load / loadstring 还在,但它们跑在同一个沙箱里(loadstring("x = 1")() 同样被拒)。cjson、cmsgpack、struct、bit 可用。
x = 1(创建全局变量) → ERR Attempt to modify a readonly table
_G.y = 1 → ERR Attempt to modify a readonly table
string.foo = 1(改标准库表) → ERR Attempt to modify a readonly table
redis.foo = 1(改 redis 表) → ERR Attempt to modify a readonly table
local z = 1(局部变量) → 1
setmetatable(_G, {}) → ERR Attempt to modify a readonly table
os = nil → ERR Attempt to modify a readonly table创建全局变量、改标准库表、改 redis 表、给 _G 设元表,全部报 Attempt to modify a readonly table。脚本之间不能通过全局变量互相传东西——要传就走 Redis 里的 key。
SET k v → OK KEYS * → OK FLUSHDB → OK CONFIG GET dir → ERR: ERR This Redis command is not allowed from script CONFIG SET maxmemory 0 → ERR: ERR This Redis command is not allowed from script ACL WHOAMI → ERR: ERR This Redis command is not allowed from script SUBSCRIBE ch → ERR: ERR This Redis command is not allowed from script MONITOR → ERR: ERR This Redis command is not allowed from script WATCH k → ERR: ERR This Redis command is not allowed from script MULTI → ERR: ERR This Redis command is not allowed from script EVAL return 1 0 → ERR: ERR This Redis command is not allowed from script SAVE → ERR: ERR This Redis command is not allowed from script BGSAVE → ERR: ERR This Redis command is not allowed from script DEBUG SLEEP 0 → ERR: ERR This Redis command is not allowed from script BLPOP q 1 → OK SCRIPT FLUSH → ERR: ERR This Redis command is not allowed from script CLIENT LIST → ERR: ERR This Redis command is not allowed from script SLAVEOF NO ONE → ERR: ERR This Redis command is not allowed from script
CONFIG、ACL、SUBSCRIBE、MONITOR、WATCH、MULTI、EVAL、SAVE、DEBUG、SCRIPT、CLIENT、SLAVEOF 被拒;SET、KEYS、FLUSHDB、BLPOP 可以。这是「不许改服务器状态、不许阻塞连接」的一条线,不是「不许破坏数据」的线——所以能执行 EVAL 的人,能做的数据破坏跟他自己直接执行命令一样多,取决于他的 ACL。(没有测 SHUTDOWN 等其余命令。)
luser 的脚本 GET app:1 → None luser 的脚本 GET other:1 → ERR ACL failure in script: No permissions to access a key luser 的脚本 DEL app:1 → ERR ACL failure in script: User luser has no permissions to run the 'del' command luser 的脚本 KEYS * → ERR ACL failure in script: User luser has no permissions to run the 'keys' command luser 的脚本 SET app:2(KEYS 里没声明该 key) → OK luser 的脚本 pcall(DEL app:1) → ERR: ERR ACL failure in script: User luser has no permissions to run the 'del' command EVAL_RO 里 SET → ERR Write commands are not allowed from read-only scripts.
脚本里每次 redis.call 都按调用者的 ACL 检查(命令和 key 都查)。redis.pcall 把 ACL 失败作为错误值返回而不是终止脚本。没在 KEYS 里声明的 key,只要符合 ACL 的 key 模式仍然可以访问(单机下的实测)。EVAL_RO 里写命令被拒。
EVAL return 1 0 -@scripting:拒绝 只禁 4 条 EVAL 系列:拒绝 EVALSHA aaaaaaaaaaaaaaaa -@scripting:拒绝 只禁 4 条 EVAL 系列:拒绝 SCRIPT LOAD return 1 -@scripting:拒绝 只禁 4 条 EVAL 系列:允许 FUNCTION LOAD #!lua name -@scripting:拒绝 只禁 4 条 EVAL 系列:允许 FCALL f 0 -@scripting:拒绝 只禁 4 条 EVAL 系列:允许 FUNCTION FLUSH -@scripting:拒绝 只禁 4 条 EVAL 系列:允许 @scripting 分类里的命令: fcall eval evalsha eval_ro fcall_ro evalsha_ro function|flush function|list function|stats function|help function|dump function|kill function|restore function|load function|delete script|kill script|debug script|help script|flush script|exists script|load
脚本相关的命令有 21 条(见上面 @scripting 的完整列表)。只禁 eval 系列四条,FUNCTION LOAD 后 FCALL 仍然能跑 Lua。要禁就用 -@scripting,然后 ACL DRYRUN 验一遍。
busy-reply-threshold 默认值: 5000 ms 纯死循环:另一个连接的 PING 等了 390 ms 才得到 → BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. SCRIPT KILL → OK;死循环那条连接得到 → ERR Script killed by user with SCRIPT KILL... 先 SET 再死循环:PING → BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. SCRIPT KILL → UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a ha SHUTDOWN NOSAVE → 连接断开(进程退出) EOFError 进程还在运行吗: False
脚本运行时服务器是单线程忙碌的,其它连接的命令要等。超过 busy-reply-threshold(默认 5000 ms;别名 lua-time-limit)后,其它连接得到 BUSY,只能 SCRIPT KILL 或 SHUTDOWN NOSAVE。脚本一旦写过数据,SCRIPT KILL 返回 UNKILLABLE,只剩 SHUTDOWN NOSAVE。所以没有写权限的用户跑死循环,伤害是可恢复的;有写权限的,只能停服务。