问题的背景:参数注入
在shell中,我们希望将参数传递给子命令,如下面的例子: 假设我们有一个http服务,它接受一个cmd 参数,并将其传递给bash -c 执行:
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
|