我们将学习一下如何使用正确的姿势通过node调用系统命令,从而避免命令注入的安全漏洞。
通常我们调用系统命令使用非常典型的方式child_process.exec来执行,通过一个命令字符串,它将回调返回一个错误或命令的结果。
代码如下:
child_process.exec('ls', function (err, data) {
console.log(data);
});
但是假如你的命令行需要依赖用户输入该怎么办?显而易见的解决方案是把用户输入的值作为你命令的一部分,使用字符串拼接的方式来构建你的整条命令。
代码如下:
var path = "user input";
child_process.exec('ls -l ' + path, function (err, data) {
console.log(data);
});
有没有闻到很坏的味道?
我们都知道,严谨、安全的程序应该不要相信任何输入源,因为这样有可能会引入不是你想要的可执行代码(注入代码)。
以这里的例子为例,在执行child_process.exec的时候,底层其实是调用/ bin/sh,而不是目标程序。也就是说,发送的命令就会传递到/bin/sh 进程作为shell命令。child_process.exec有一个误导性的名称:——bash的解析器,而不是启动器。这意味着只要用户输入的命令是shell支持的,都可以在系统级别执行,当然也可能会含有破坏性的命令。
系统调用
[pid 25170] execve(“/bin/sh”, [“/bin/sh”, “-c”, “ls -l user input”], [/* 16 vars */]
例如,攻击者可以使用;来结束语句,并开始另一个命令,他们还可以使用引号或者$()来运行子命令等等很多潜在的危险。
那么正确的姿势是怎样的呢?
execFile / spawn
调用spawn 和 execFile这样的接口,将所有的命令都作为额外参数数组,而不是直接shell环境下执行的命令,另外不要直接操纵原本要运行的命令。
我们来修改下例子,看看如何使用spawn 和 execFile。
child_process.execFile
var child_process = require('child_process');
var path = "."
child_process.execFile('/bin/ls', ['-l', path], function (err, result) {
console.log(result)
});
系统调用
[pid 25565] execve("/bin/ls", ["/bin/ls", "-l", "."], [/* 16 vars */]
child_process.spawn
var child_process = require('child_process');
var path = "."
var ls = child_process.spawn('/bin/ls', ['-l', path])
ls.stdout.on('data', function (data) {
console.log(data.toString());
});
系统调用
[pid 26883] execve(“/bin/ls”, [“/bin/ls”, “-l”, “.”], [/* 16 vars */
为什么这种方式不易受到命令注入呢?
通过系统调用来对比,可以发现我们的目标程序是execve的参数。这意味着用户不能在shell运行子命令,因为/bin/ls解析的时候只会把path当成字符串参数,所以里面的引号或分号都起不了作用。类似于使用参数化解决注入问题的思想,在SQL注入中也经常使用到,SQL查询将查询条件使用问好?进行参数化,而不是通过拼接字符串的方式查询。
不过使用execFile / spawn并不总是安全的:例如,使用 spawn or execFile运行/bin/find ,并直接通过用户输入,依然可能会导致系统被接管,而使文件被任意读/写。
几点忠告: