Python 管道以避免 Shell¶
您应该先阅读shell 注入文档,然后再阅读本文档。
很多时候,我们的代码库使用 shell=True 是因为这样很方便。shell 提供了无需在内存中缓冲即可进行管道操作的能力,并且允许恶意用户在合法命令运行后链接额外的命令。
错误¶
这是一个简单的函数,它使用 curl 从网站抓取页面,并将其直接通过管道传输到 wordcount 程序,以告诉我们 HTML 源代码中有多少行。
def count_lines(website):
return subprocess.check_output('curl %s | wc -l' % website, shell=True)
#>>> count_lines('www.google.com')
#'7\n'
(顺便说一句,这个输出是正确的——谷歌的 HTML 源代码确实有 7 行。)
该函数是不安全的,因为它使用了 shell=True,这允许shell 注入。指示您的代码获取网站 ; rm -rf / 的用户可能会对您的机器造成可怕的破坏。
如果我们将函数转换为使用 shell=False,它就无法工作。
def count_lines(website):
args = ['curl', website, '|', 'wc', '-l']
return subprocess.check_output(args, shell=False)
# >>> count_lines('www.google.com')
# curl: (6) Could not resolve host: |
# curl: (6) Could not resolve host: wc
# Traceback (most recent call last):
# File "<stdin>", line 3, in count_lines
# File "/usr/lib/python2.7/subprocess.py", line 573, in check_output
# raise CalledProcessError(retcode, cmd, output=output)
# subprocess.CalledProcessError: Command
# '['curl', 'www.google.com', '|', 'wc', '-l']' returned non-zero exit status 6
当 shell=False 时,管道没有任何特殊含义,因此 curl 尝试下载名为“|”的网站。这并不能解决问题,反而使问题变得比以前更糟糕。
如果我们不能在 shell=False 时依赖管道,我们应该怎么做呢?
正确¶
def count_lines(website):
args = ['curl', website]
args2 = ['wc', '-l']
process_curl = subprocess.Popen(args, stdout=subprocess.PIPE,
shell=False)
process_wc = subprocess.Popen(args2, stdin=process_curl.stdout,
stdout=subprocess.PIPE, shell=False)
# Allow process_curl to receive a SIGPIPE if process_wc exits.
process_curl.stdout.close()
return process_wc.communicate()[0]
# >>> count_lines('www.google.com')
# '7\n'
我们不是调用一个运行我们每个程序的 shell 进程,而是分别运行它们,并将 curl 的 stdout 连接到 wc 的 stdin。我们指定 stdout=subprocess.PIPE,这告诉 subprocess 将该输出发送到相应的文件句柄。
将管道视为文件描述符(如果需要,您实际上可以使用 FD),如果另一端没有连接任何东西,它们可能会在读取和写入时阻塞。这就是我们使用 communicate() 的原因,它会读取输出直到 EOF,然后等待进程终止。您通常应该避免直接读取和写入管道,除非您确实知道自己在做什么——很容易陷入死锁的情况。
请注意,communicate() 会在内存中缓冲结果——如果这不是您想要的,请为 stdout 使用文件描述符将该输出通过管道传输到文件中。
后果¶
使用管道可帮助您在更复杂的情况下避免 shell 注入
使用管道可能会导致死锁(在 Python 或 shell 中)