安全地使用子进程¶
许多常见任务都涉及与操作系统交互——我们编写了大量用于配置、修改或以其他方式控制系统的代码,其中存在许多潜在的陷阱。
通过 shell 调用另一个程序是很常见的需求。在大多数情况下,您会希望将参数传递给这个程序。这是一个用于 ping 另一台服务器的简单函数。
错误¶
def ping(myserver):
return subprocess.check_output('ping -c 1 %s' % myserver, shell=True)
>>> ping('8.8.8.8')
64 bytes from 8.8.8.8: icmp_seq=1 ttl=58 time=5.82 ms
这个程序只是将一个字符串作为命令提供给 shell,shell 会不加思索地运行它。输入参数之间没有语义上的分离,即 shell 无法分辨命令应该在哪里结束,参数从哪里开始。
如果 myserver 参数由用户控制,这可以用于执行任意程序,例如 rm
>>> ping('8.8.8.8; rm -rf /')
64 bytes from 8.8.8.8: icmp_seq=1 ttl=58 time=6.32 ms
rm: cannot remove `/bin/dbus-daemon': Permission denied
rm: cannot remove `/bin/dbus-uuidgen': Permission denied
rm: cannot remove `/bin/dbus-cleanup-sockets': Permission denied
rm: cannot remove `/bin/cgroups-mount': Permission denied
rm: cannot remove `/bin/cgroups-umount': Permission denied
...
如果您选择测试,我们建议您选择一个破坏性较小的命令,而不是“rm -rf /”,例如“touch helloworld.txt”。
正确¶
此函数可以安全地重写
def ping(myserver):
args = ['ping', '-c', '1', myserver]
return subprocess.check_output(args, shell=False)
我们的函数不是将字符串传递给子进程,而是传递一个字符串列表。ping 程序单独获取每个参数(即使参数中包含空格),因此 shell 不会处理用户在 ping 命令结束后提供的其他命令。您不必显式设置 shell=False——它是默认值。
如果我们用与之前相同的输入进行测试,ping 命令将 myserver 值正确地解释为单个参数,并抱怨说这是一个非常奇怪的主机名,尝试 ping 它。
>>> ping('8.8.8.8; rm -rf /')
ping: unknown host 8.8.8.8; rm -rf /
这个程序现在安全得多,即使它必须允许用户提供的输入。
后果¶
如果您使用 shell=True,您的代码极有可能存在漏洞
即使*您的*代码没有漏洞,下一个维护者也很容易引入漏洞。
Shell 注入是任意代码执行——一个有能力的攻击者将利用这些漏洞来攻陷您系统的其余部分。