问题的背景:参数注入
在shell中,我们希望将参数传递给子命令,如下面的例子:
假设我们有一个http服务,它接受一个cmd
参数,并将其传递给bash -c
执行:
// curl 'localhost:8080/demo?cmd=echo hello'
handler(req,res){
const cmd = req.query.cmd;
const process = child_process.exec(`bash -c "${cmd}"`);
// ...
}
显然,因为cmd
将被拼接在字符串中,所以上面的处理方式有很明显的问题:如果cmd
中含有双引号,或者其他特殊字符,甚至cmd=':";rm -rf /
, 那么执行的命令就变成了bash -c ":";rm -rf /
,是极其危险的。
其实,这在编程领域是非常常见的注入问题,和SQL注入是同样的道理:参数将被拼接到命令中执行,因此不能信任参数。
解决方法
解决方法就是对要拼接的参数进行转义,防止其中的任何特殊字符造成期望之外的结果。
比如,bash -c "${cmd}"
的含义是: cmd
将作为一个完整的字符串被双引号包围。所以,需要对cmd
进行转义:
function quoteShell(cmd){
return cmd.replaceAll("\\", "\\\\").replaceAll("$", "\\$").replaceAll("`", "\\`").replaceAll("\"", "\\\"").replaceAll("\n", "\\n")
}
上面的例子一种常见的情况,即参数只会被命令行解析一次,因此需要保证命令行解析的结果就是原始的参数字符串。所以,本质上quote
函数可以视为bash
解析参数的逆过程。
还有另外一种情况,参数通过printf
进行传递:
printf "${cmd}" | bash
在这种情况下,参数不仅要被bash
解析,还要被printf
解析,如何保证经过这两个解析过程之后,得到原始的参数字符串?
我们分析解析的过程:首先是bash解析,然后printf解析bash解析的参数,即实际参数 -> bash -> printf -> 原始参数
,所以,我们将这条链反转过来就是如何从原始参数到实际参数的过程: 原始参数->printf->bash->实际参数
。因此,我们需要定义quotePrintf
:
function quotePrintf(cmd){
cmd = cmd.replaceAll("\\","\\\\").replaceAll("%","%%")
return quoteShell(cmd)
}
- 先将printf中的特殊字符\和%转义
- 再将转义后的字符串进行
quoteShell
注意:对printf来说,"
不是特殊字符,那只是bash的特殊字符。
例子:
quoteShell:
'\n' -> '\\n'
$A -> "\$A"
"\n -> \"\\n"
quotePrintf:
'\n' -> '\\\\n'
$A -> \$A
"\n -> \"\\\\n
%s -> %%s