目录
在kamailio.cfg.template脚本中,执行redis的命令都是通过redis_cmd来向redis服务器发送的,并且脚本里面都是对原本的命令以及参数都是拼接,所以担心存在注入问题。
我们知道,对于mysql来说,她有一个参数化防注入机制,有专门的"预编译语法",也就是提前固定命令机构,后续绑定参数,而redis是不具备这个机制的。
但是通过查询,redis有RESP解析规则,并且kamailio中也是遵循这个协议来发送redis命令的。RESP协议会按照固定的格式解析"命令名+参数",会把第一个元素当作命令,其余当作这个命令的参数。
这里需要介绍一下RESP:是Redis 客户端与服务器之间的通信协议,RESP 可以序列化不同的数据类型,包括整数、字符串和数组。 它还具有一种针对错误的特定类型。 客户端以字符串数组的形式向 Redis 服务器发送请求。 数组的内容是服务器应执行的命令及其参数。 服务器的回复类型是命令特定的。
(服务端具体怎么通过协议解析命令详见第二部分)
接着通过源码、redis官方文档、实验测试三部分来证明redis_cmd底层的实现已经杜绝了注入问题(即使是拼接)
一.源码查看
kamailio中的redis_cmd底层是实现了参数化传递防注入攻击的。从源码来看,有w_redis_cmd3(cmd4,cmd5.....)->redisc_exec->redisvCommand->redisvAppendCommand->redisvFormatCommand。最终跳转到redisvFormatCommand函数中。
在这个函数里面把命令模板和参数解析成"命令名+独立参数数组",而不是拼接成普通字符串。具体实现是:首先用curargv来独立存储每个参数,遍历c(也就是format)从参数列表里面取出实际参数,把参数内容追加到curarg,所以说在这里是当值存储的,不会解析redis语法,遍历完后,所有参数都会被存入curargv数组,成为独立元素。最后他会遍历每个参数,原样写入不解析任何语法。所以redis服务器收到之后,假设第一个参数SET是命令名,后续的参数中即使有flushall也会被当作值。
下面的代码只是redisvFormatCommand函数的一部分,主要提取了实现 Redis 命令注入防护的核心逻辑片段,完整的函数在下图所示的文件中

cpp
/*
* 核心功能:将格式化命令字符串解析为Redis RESP协议格式的命令
* 防注入核心:将所有参数(含恶意字符)封装为RESP协议的"参数数组",而非拼接成可执行的多命令
* 参数说明:
* target - 输出最终的RESP协议命令字符串
* format - 命令格式模板(如"SET %s %s")
* ap - 可变参数列表(模板中%s/%b对应的实际参数)
*/
int redisvFormatCommand(char **target, const char *format, va_list ap) {
const char *c = format; // 遍历格式模板的指针
char **curargv = NULL; // 存储拆分后的参数数组(核心防注入载体)
int argc = 0; // 参数数组的长度
int totlen = 0; // 最终RESP命令的总长度
hisds curarg = hi_sdsempty(); // 临时存储当前解析的参数
int touched = 0; // 标记当前参数是否有内容
if (target == NULL) return -1;
if (curarg == NULL) return -1;
/* ========== 核心步骤1:解析格式模板,拆分参数(防注入关键) ========== */
while(*c != '\0') {
// 非占位符(%)的普通字符:逐字符拼接为当前参数
if (*c != '%' || c[1] == '\0') {
// 遇到空格:结束当前参数,存入参数数组(按空格拆分参数)
if (*c == ' ') {
if (touched) {
// 扩容参数数组,将当前参数存入
char **newargv = hi_realloc(curargv,sizeof(char*)*(argc+1));
if (newargv == NULL) goto memory_err;
curargv = newargv;
curargv[argc++] = curarg; // 每个参数独立存储,不拼接
totlen += bulklen(hi_sdslen(curarg));
// 重置临时参数存储,准备解析下一个参数
curarg = hi_sdsempty();
if (curarg == NULL) goto memory_err;
touched = 0;
}
} else {
// 非空格字符:拼接到当前参数(恶意字符仅作为普通字符存储)
hisds newarg = hi_sdscatlen(curarg,c,1);
if (newarg == NULL) goto memory_err;
curarg = newarg;
touched = 1;
}
} else {
/* ========== 核心步骤2:处理占位符(%s/%b等),安全填充参数 ========== */
char *arg;
size_t size;
hisds newarg = curarg;
switch(c[1]) {
case 's': // 字符串参数:安全获取参数,拼接到当前参数(不解析恶意命令)
arg = va_arg(ap,char*);
size = strlen(arg);
if (size > 0)
newarg = hi_sdscatlen(curarg,arg,size);
break;
case 'b': // 二进制参数:按长度读取,避免解析\r\n等协议分隔符
arg = va_arg(ap,char*);
size = va_arg(ap,size_t);
if (size > 0)
newarg = hi_sdscatlen(curarg,arg,size);
break;
case '%': // 转义%:仅作为普通字符存储,不解析为占位符
newarg = hi_sdscat(curarg,"%");
break;
}
if (newarg == NULL) goto memory_err;
curarg = newarg;
touched = 1;
c++; // 跳过占位符的第二个字符(如%s的s)
}
c++;
}
/* ========== 核心步骤3:收尾最后一个参数,存入参数数组 ========== */
if (touched) {
char **newargv = hi_realloc(curargv,sizeof(char*)*(argc+1));
if (newargv == NULL) goto memory_err;
curargv = newargv;
curargv[argc++] = curarg; // 最后一个参数存入数组
totlen += bulklen(hi_sdslen(curarg));
} else {
hi_sdsfree(curarg);
}
/* ========== 核心步骤4:封装为RESP协议(最终防注入保障) ========== */
// 计算RESP协议头长度(*argc\r\n)
totlen += 1+countDigits(argc)+2;
char *cmd = hi_malloc(totlen+1);
if (cmd == NULL) goto memory_err;
// 1. 写入RESP协议头:*参数个数\r\n(标识这是一个参数数组)
int pos = sprintf(cmd,"*%d\r\n",argc);
// 2. 遍历参数数组,按RESP格式写入每个参数($长度\r\n值\r\n)
for (int j = 0; j < argc; j++) {
// 写入参数长度:$长度\r\n
pos += sprintf(cmd+pos,"$%zu\r\n",hi_sdslen(curargv[j]));
// 写入参数内容(含恶意字符的参数仅作为普通字符串)
memcpy(cmd+pos,curargv[j],hi_sdslen(curargv[j]));
pos += hi_sdslen(curargv[j]);
hi_sdsfree(curargv[j]);
// 写入参数结尾分隔符:\r\n(转义恶意的\r\n,避免解析为新命令)
cmd[pos++] = '\r';
cmd[pos++] = '\n';
}
cmd[pos] = '\0';
/* ========== 资源释放与结果返回 ========== */
hi_free(curargv);
*target = cmd; // 最终的RESP协议命令(防注入后的安全格式)
return totlen;
}
二.官方文档说明
1.概述
接着redis官方提供了对于RESP的详细介绍,包括服务器端收到消息会怎么解析,以及客户端以怎样的形式去发送redis的命令,下面是链接,可以查看
https://redis.io/docs/latest/develop/reference/protocol-spec/
文中介绍了该协议的基本概念:RESP 可以序列化不同的数据类型,包括整数、字符串和数组。 它还具有一种针对错误的特定类型。 客户端以字符串数组的形式向 Redis 服务器发送请求。 数组的内容是服务器应执行的命令及其参数。 服务器的回复类型是命令特定的。
RESP 是二进制安全的,使用前缀长度传输大批量数据,因此无需处理从一个进程传输到另一个进程的批量数据。
2.RESP协议描述
这是服务器对于接收到消息的接收处理,他说:数组中的第一个(有时也是第二个)批量字符串就是命令的名称,数组的后续元素是命令的参数。并且用\r\n分隔其各部分。

3.向Redis服务器发送命令
这里定义了客户端向服务器发送命令的格式

三.测试验证
1.字符串拼接注入测试
核心逻辑:直接拼接恶意命令; KEYS *;到 SET 字符串中,模拟注入漏洞,验证 Redis 会执行注入命令。
redis日志显示:在kamailio.cfg.template脚本中request_route中添加以下代码:
bash
# 1. 定义恶意注入内容
$var(malicious) = "test_key; KEYS *;"; # 注入KEYS *(只读,无破坏性)
# 2. 危险写法:字符串拼接SET命令(留注入漏洞)
$var(danger_cmd) = "SET " + $var(malicious) + " 123"; # 拼接成:SET test_key; KEYS *; 123
$var(res) = redis_cmd("db0", "$var(danger_cmd)", "test_danger");
redis日志显示:

可以看到他把这个命令拆分成了多个参数,接着在redis中直接执行这个命令发现报语法错误,所以注入的redis命令无法执行或生效,就是没有注入成功。

那如果把冒号改成空格,能否注入成功呢
bash
# 1. 定义恶意注入内容
$var(malicious) = "test_key KEYS *"; # 注入KEYS *(只读,无破坏性)
# 2. 危险写法:字符串拼接SET命令(留注入漏洞)
$var(danger_cmd) = "SET " + $var(malicious) ;
$var(res) = redis_cmd("db0", "$var(danger_cmd)", "test_danger");
可以看到命令仍然无法被拆分成正确的格式,并且在redis中执行该命令,也是语法错误,注入失败。

综上所述,对于redis_cmd来说,对于命令的发送和解析遵循底层的RESP协议,即使是拼接命令,redis收到命令之后,第一个参数当作命令的名称,后续输入的内容都是命令的参数,不会发生注入。
2.占位符参数化注入测试
核心逻辑:用%s占位符传递含恶意内容的参数,验证 Redis 仅把恶意内容当作 key 值,不执行注入命令。
在脚本中添加以下内容
bash
# 1. 定义恶意内容(和场景1完全一样)
$var(malicious) = "test_key KEYS *";
# 2. 安全写法:占位符参数化(最简单的SET key value)
$var(res) = redis_cmd("db0", "SET %s %s", "$var(malicious)", "123","abc"); # 占位符传key和value
日志中图片显示KEYS 并没有作为一个单独命令,而是作为一个参数

查询keys的value,发现KEYS是添加到后续原本要设置的key后面,设置的value是123。

综上所述,如果是用占位符参数化防止注入,也是可以成功的,还是依赖于底层的RESP协议,redis收到命令之后,第一个参数当作命令的名称,后续输入的内容都是命令的参数,不会发生注入。
3.RESP协议二进制注入测试
核心逻辑:传递含\r\n(RESP 协议分隔符)的恶意二进制内容,验证 Redis 不解析为新命令,仅当作普通字符。
在脚本中添加以下内容:
bash
# 1. 定义恶意内容(和场景1完全一样)
$var(malicious_bin) = "hack_key\r\n$3\r\nSET\r\n$5\r\nhack_val\r\n$3\r\n123\r\n";
# 2. 安全写法:占位符传递二进制恶意内容(最简单的SET)
$var(res) = redis_cmd("db0", "SET %s %s", "$var(malicious_bin)", "456", "abc");
原本是要执行两次SET命令的,但是redis日志显示还是把后面的当作一整个参数。

可以发现设置了"hack_key\r\n3\\r\\nSET\\r\\n5\r\nhack_val\r\n$3\r\n123\r\n"整体的值为456

综上所述,由于RESP底层对于命令的发送和解析是依赖\r\n分隔,模拟这样的二进制内容,仍然会把命令名称后面的内容当作参数,无法注入成功。
四.结论
所以最后的结论就是,通过查看源码以及官方文档,还有进行不同场景下的实验测试,发现redis_cmd底层设计了参数处理逻辑和RESP协议封装,天然就具备防御命令注入的能力。
redis_cmd会将第一个元素识别为Redis命令名,后续的所有内容无论是含(;/\n)都会当作当前命令的参数,不会被解析为独立的命令。
RESP协议的封装,redis_cmd会将所有参数按协议规范封装(长度\\r\\n值\\r\\n),对于参数中的\\r\\n协议分隔符会自动转义为普通字符,构造含 \\r\\n3\r\nsET\r\n$5\r\nhack_val\r\n 的二进制恶意内容,最终仅作为 SET 命令的 key字符串存储,Redis 未解析出注入的 sET hack_val 123 命令。
