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 中)

参考