【问题标题】:Actual meaning of 'shell=True' in subprocess子进程中'shell = True'的实际含义
【发布时间】:2011-03-11 11:46:45
【问题描述】:

我正在使用subprocess 模块调用不同的进程。不过,我有一个问题。

在以下代码中:

callProcess = subprocess.Popen(['ls', '-l'], shell=True)

和

callProcess = subprocess.Popen(['ls', '-l']) # without shell

两者都有效。阅读文档后,我知道shell=True 表示通过shell 执行代码。这意味着在缺席的情况下,直接启动该过程。

那么对于我的情况,我应该更喜欢什么 - 我需要运行一个进程并获取它的输出。从 shell 内部或外部调用它有什么好处。

【问题讨论】:

  • 第一个命令不正确:-l 被传递给/bin/sh(shell)而不是ls 程序on Unix if shell=True。在大多数情况下,字符串参数应与 shell=True 一起使用,而不是列表。
  • re "进程直接启动": Wut?
  • 声明“两者都有效”。关于这两个电话是不正确和误导的。调用的工作方式不同。只是从shell=True 切换到False,反之亦然是错误的。来自docs:“在 shell=True 的 POSIX 上,(...) 如果 args 是一个序列,则第一项指定命令字符串,并且任何附加项都将被视为 shell 本身的附加参数。”。在 Windows 上,有 automatic conversion,这可能是不受欢迎的。
  • 有用的说明:你可以提供一个列表来调用/Popen,但它会默默地忽略除了列表的第一项之外的所有内容。 Python 3.5/Linux。

标签: python subprocess


【解决方案1】:

不通过 shell 调用的好处是您不会调用“神秘程序”。在 POSIX 上,环境变量 SHELL 控制作为“shell”调用的二进制文件。在 Windows 上,没有 bourne shell 后代,只有 cmd.exe。

因此调用 shell 会调用用户选择的程序,并且是平台相关的。一般来说,避免通过 shell 调用。

通过 shell 调用确实允许您根据 shell 的通常机制扩展环境变量和文件 glob。在 POSIX 系统上,shell 将文件 glob 扩展为文件列表。在 Windows 上,无论如何,shell 不会扩展文件 glob(例如,“*.*”)(但命令行上的环境变量由 cmd.exe 扩展)。

如果您认为您需要环境变量扩展和文件 glob,请研究 1992 年对通过 shell 执行子程序调用的网络服务的ILS 攻击。示例包括涉及ILS 的各种sendmail 后门。

总之,使用shell=False。

【讨论】:

  • 感谢您的回答。虽然我真的不在那个我应该担心漏洞利用的阶段,但我理解你的意思。
  • 如果你一开始不小心,再多的担心也无助于你以后的追赶。 ;)
  • 关于$SHELL的说法不正确。引用 subprocess.html:“在带有 shell=True 的 Unix 上,shell 默认为 /bin/sh。” (不是$SHELL)
  • @user2428107:是的,如果您在 Perl 上使用反引号调用,那么您使用的是 shell 调用并打开了相同的问题。如果您想以安全的方式调用程序并捕获输出,请使用 3+ arg open。
  • 我猜你的意思是IFS?我没有发现与 Sendmail 漏洞相关的“ILS”,而对 IFS 的不当处理是 Sendmail 早期版本中众所周知的攻击媒介。
【解决方案2】:
>>> import subprocess
>>> subprocess.call('echo $HOME')
Traceback (most recent call last):
...
OSError: [Errno 2] No such file or directory
>>>
>>> subprocess.call('echo $HOME', shell=True)
/user/khong
0

将 shell 参数设置为真值会导致子进程产生一个中间 shell 进程,并告诉它运行命令。换句话说,使用中间 shell 意味着在命令运行之前处理命令字符串中的变量、glob 模式和其他特殊的 shell 功能。这里,在示例中,$HOME 在 echo 命令之前被处理。实际上,这是带有shell扩展的命令的情况,而命令 ls -l 被认为是一个简单的命令。

来源:Subprocess Module

【讨论】:

  • 同意。这是我理解 shell=True 含义的一个很好的例子。
  • 将 shell 参数设置为真值会导致子进程生成一个中间 shell 进程,并告诉它运行命令 天哪,这说明了一切。为什么这个答案不被接受???为什么?
  • 我认为问题在于调用的第一个参数是一个列表,而不是一个字符串,但是如果 shell 为 False,则会给出错误。将命令更改为列表将使其工作
  • 抱歉,我之前的评论还没写完。需要明确的是:我经常看到子进程与 shell = True 一起使用,并且命令是一个字符串,例如'ls -l', (我希望避免这个错误)但是 subprocess 需要一个列表(和一个字符串作为一个元素列表)。要在不调用 shell(和 security issues with that )的情况下运行,请使用列表 subprocess.call(['ls', '-l'])
【解决方案3】:

此处显示了 Shell=True 可能出错的示例

>>> from subprocess import call
>>> filename = input("What file would you like to display?\n")
What file would you like to display?
non_existent; rm -rf / # THIS WILL DELETE EVERYTHING IN ROOT PARTITION!!!
>>> call("cat " + filename, shell=True) # Uh-oh. This will end badly...

在此处查看文档:subprocess.call()

【讨论】:

  • 这个链接很有用。正如链接所述:执行包含来自不受信任来源的未经处理的输入的 shell 命令会使程序容易受到 shell 注入的攻击,这是一个严重的安全漏洞,可能导致任意命令执行。因此,在命令字符串是从外部输入构造的情况下,强烈建议不要使用 shell=True。
  • 请注意,即使是shell=False,您仍然需要小心。例如,如果filename 是"-r",或者如果它是类似../../private/path/filename.txt 的路径,call(["rm", filename1, filename2]) 可能会出现意外行为。使用双破折号并确保文件名不是您不希望的路径。
【解决方案4】:

通过 shell 执行程序意味着传递给程序的所有用户输入都根据被调用 shell 的语法和语义规则进行解释。充其量,这只会给用户带来不便,因为用户必须遵守这些规则。例如,必须对包含特殊 shell 字符(如引号或空格)的路径进行转义。在最坏的情况下,它会导致安全漏洞,因为用户可以执行任意程序。

shell=True 有时可以方便地使用特定的 shell 功能,例如分词或参数扩展。但是,如果需要这样的功能,请使用为您提供的其他模块(例如,os.path.expandvars() 用于参数扩展或shlex 用于分词)。这意味着更多的工作,但避免了其他问题。

简而言之:一定要避免shell=True。

【讨论】:

    【解决方案5】:

    此处的其他答案充分解释了subprocess 文档中也提到的安全警告。但除此之外,启动 shell 以启动您想要运行的程序的开销通常是不必要的,而且对于您实际上不使用任何 shell 功能的情况来说绝对是愚蠢的。此外,额外隐藏的复杂性应该会吓到你,尤其是如果你不是很熟悉 shell 或它提供的服务。

    在与 shell 的交互非常重要的地方,您现在需要 Python 脚本的读者和维护者(可能是也可能不是您未来的自己)理解 Python 和 shell 脚本。记住 Python 的座右铭“显式优于隐式”; 即使 Python 代码比等效(通常非常简洁)的 shell 脚本更复杂,你最好还是删除shell 并用本机 Python 构造替换功能。尽量减少在外部流程中完成的工作并尽可能在您自己的代码中进行控制通常是一个好主意,因为它提高了可见性并降低了(想要的或不需要的)副作用的风险。

    通配符扩展、变量插值和重定向都可以简单地替换为原生 Python 结构。在 Python 中无法合理重写部分或全部内容的复杂 shell 管道可能是您可以考虑使用 shell 的一种情况。您仍应确保了解性能和安全影响。

    在琐碎的情况下,为了避免shell=True,只需替换

    subprocess.Popen("command -with -options 'like this' and\\ an\\ argument", shell=True)
    

    与

    subprocess.Popen(['command', '-with','-options', 'like this', 'and an argument'])
    

    注意第一个参数是如何传递给execvp() 的字符串列表,以及如何引用字符串和反斜杠转义shell 元字符通常是不必要的(或有用或正确的)。 也可以看看When to wrap quotes around a shell variable?

    如果您不想自己解决这个问题,shlex.split() 函数可以为您解决这个问题。它是 Python 标准库的一部分,但当然,如果您的 shell 命令字符串是静态的,您可以在开发期间只运行一次,然后将结果粘贴到您的脚本中。

    顺便说一句,如果subprocess 包中的一个更简单的包装器可以满足您的需求,您通常希望避免使用Popen。如果你有足够新的 Python,你可能应该使用subprocess.run。

    • 使用check=True,如果您运行的命令失败,它将失败。
    • 使用stdout=subprocess.PIPE,它将捕获命令的输出。
    • 使用text=True(或者有点晦涩,使用同义词universal_newlines=True),它会将输出解码为适当的Unicode字符串(否则,在Python 3上,系统编码中只是bytes)。

    如果不是,对于许多任务,您希望check_output 获取命令的输出,同时检查它是否成功,或者如果没有要收集的输出,则需要check_call。

    我将引用 David Korn 的一句话作为结尾:“编写可移植的 shell 比编写可移植的 shell 脚本更容易。”甚至 subprocess.run('echo "$HOME"', shell=True) 也不能移植到 Windows。

    【讨论】:

    • 我认为这句话来自拉里沃尔,但谷歌告诉我不是这样。
    • 这是高谈阔论 - 但没有更换的技术建议:我在这里,在 OS-X 上,试图获取我通过 'open' 启动的 Mac 应用程序的 pid: process = subprocess.Popen( '/usr/bin/pgrep -n ' + app_name, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) app_pid, err = process.communicate() --- 但它不工作,除非我'将使用 shell=True。现在呢?
    • 有很多关于如何避免shell=True的问题,其中许多问题的答案都很好。你碰巧选择了关于why的那个。
    • @MottiShneor 感谢您的反馈;添加简单示例
    【解决方案6】:

    上面的 Anwser 解释正确,但不够直截了当。 让我们使用ps 命令看看会发生什么。

    import time
    import subprocess
    
    s = subprocess.Popen(["sleep 100"], shell=True)
    print("start")
    print(s.pid)
    time.sleep(5)
    s.kill()
    print("finish")
    

    运行它并显示

    start
    832758
    finish
    

    然后您可以在finish 之前使用ps -auxf > 1,然后在finish 之后使用ps -auxf > 2。这是输出

    1

    cy         71209  0.0  0.0   9184  4580 pts/6    Ss   Oct20   0:00  |       \_ /bin/bash
    cy        832757  0.2  0.0  13324  9600 pts/6    S+   19:31   0:00  |       |   \_ python /home/cy/Desktop/test.py
    cy        832758  0.0  0.0   2616   612 pts/6    S+   19:31   0:00  |       |       \_ /bin/sh -c sleep 100
    cy        832759  0.0  0.0   5448   532 pts/6    S+   19:31   0:00  |       |           \_ sleep 100
    

    看到了吗?而不是直接运行sleep 100。它实际上运行/bin/sh。它打印出来的pid实际上是/bin/sh的pid。之后如果你调用s.kill(),它会杀死/bin/sh,但sleep 仍然存在。

    2

    cy         69369  0.0  0.0 533764  8160 ?        Ssl  Oct20   0:12  \_ /usr/libexec/xdg-desktop-portal
    cy         69411  0.0  0.0 491652 14856 ?        Ssl  Oct20   0:04  \_ /usr/libexec/xdg-desktop-portal-gtk
    cy        832646  0.0  0.0   5448   596 pts/6    S    19:30   0:00  \_ sleep 100
    

    那么接下来的问题是,/bin/sh 能做什么?每个 linux 用户都知道、听到并使用它。但我敢打赌,确实有很多人并不真正了解shell 是什么。也许你也听说过/bin/bash,它们很相似。

    shell 的一个明显功能是方便用户运行 linux 应用程序。由于sh或bash之类的shell程序,您可以直接使用ls之类的命令,而不是/usr/bin/ls。它将搜索ls 所在的位置并为您运行。

    其他功能是将$之后的字符串解释为环境变量。你可以比较这两个python脚本来了解自己。

    subprocess.call(["echo $PATH"], shell=True)
    
    subprocess.call(["echo", "$PATH"])
    

    最重要的是,它可以将 linux 命令作为脚本运行。比如ifelse都是shell引入的。它不是原生 linux 命令

    【讨论】:

    • "当然shell程序也引入了环境变量的概念。"这是不正确的。环境变量是没有外壳的东西。
    • 你是对的,我用了错误的词来描述它。我改变我的说法
    【解决方案7】:

    假设您使用的是 shell=False 并将命令作为列表提供。一些恶意用户尝试注入“rm”命令。 你会看到,'rm' 将被解释为一个参数,并且'ls' 将有效地尝试找到一个名为'rm'的文件

    >>> subprocess.run(['ls','-ld','/home','rm','/etc/passwd'])
    ls: rm: No such file or directory
    -rw-r--r--    1 root     root          1172 May 28  2020 /etc/passwd
    drwxr-xr-x    2 root     root          4096 May 29  2020 /home
    CompletedProcess(args=['ls', '-ld', '/home', 'rm', '/etc/passwd'], returncode=1)
    

    shell=False 如果您没有正确控制输入,默认情况下不是安全的。你仍然可以执行危险的命令。

    >>> subprocess.run(['rm','-rf','/home'])
    CompletedProcess(args=['rm', '-rf', '/home'], returncode=0)
    >>> subprocess.run(['ls','-ld','/home'])
    ls: /home: No such file or directory
    CompletedProcess(args=['ls', '-ld', '/home'], returncode=1)
    >>>
    

    我在容器环境中编写大部分应用程序,我知道正在调用哪个 shell,并且我没有接受任何用户输入。

    所以在我的用例中,我认为没有安全风险。而且创建一长串命令要容易得多。希望我没有错。

    【讨论】:

      猜你喜欢
      • 2013-03-05
      • 2018-06-14
      • 2018-01-15
      • 2017-02-09
      • 2016-11-19
      • 1970-01-01
      • 1970-01-01
      • 2018-06-09
      相关资源
      最近更新 更多