如何避免 os.system() 调用?

145
使用os.system()时,经常需要对作为参数传递给命令的文件名和其他参数进行转义。我应该如何做到这一点?最好是能在多个操作系统/Shell上都适用,尤其是对于bash。
目前我正在做以下操作,但我相信肯定有一个库函数可以实现这个,或者至少有一个更优雅/稳健/高效的选项:
def sh_escape(s):
   return s.replace("(","\\(").replace(")","\\)").replace(" ","\\ ")

os.system("cat %s | grep something | sort > %s" 
          % (sh_escape(in_filename), 
             sh_escape(out_filename)))

编辑:我接受了使用引号的简单答案,不知道为什么我没有想到;我猜是因为我来自Windows,其中'和"的行为有些不同。

关于安全性,我理解大家的担忧,但在这种情况下,我对os.system()提供的快速简便的解决方案感兴趣,而字符串的来源要么不是用户生成的,要么至少由可信任的用户(即我)输入。


2
注意安全问题!例如,如果out_filename是foo.txt;rm -rf /,恶意用户可以添加更多由shell直接解释的命令。 - Steve Gury
6
在没有os.system的情况下,这也是有用的,例如在无法使用子进程的情况下生成shell脚本。 - Roger Pate
一个理想的 sh_escape 函数应该转义掉 ; 和空格,并通过创建一个名为 foo.txt\;\ rm\ -rf\ / 的文件来消除安全问题。 - Tom
在几乎所有情况下,您应该使用subprocess而不是os.system。调用os.system只是在寻求注入攻击。 - allyourcode
请记住,仅仅对shell参数进行转义是不够安全的,您还需要注意以破折号开头的参数或意外路径,当文件名被预期时。 - Flimm
10个回答

188

shlex.quote() 是Python 3中提供的功能,可以满足您的需求。

(如果要同时支持Python 2和Python 3,请使用 pipes.quote, 但请注意,pipes自3.10版本开始已被弃用,并计划在3.13版本中移除。)


3
由于某种原因,“pipes.quote”没有出现在pipes模块的标准库文档中。 - Day
1
两者都没有文档记录;command.mkarg在3.x中已被弃用并删除,而pipes.quote仍然存在。 - Beni Cherniavsky-Paskin
9
更正:在3.3版本中,官方文档称之为shlex.quote(),保留pipes.quote()以保持兼容性。[http://bugs.python.org/issue9723] - Beni Cherniavsky-Paskin
8
管道在 Windows 上无法使用 - 会用单引号代替双引号。 - Nux
我认为这个答案不正确。最近的Python文档说:“警告= shlex模块仅设计用于Unix shell。” - Matthew Roberts
显示剩余3条评论

90

这是我使用的:

def shellquote(s):
    return "'" + s.replace("'", "'\\''") + "'"

在传递给程序之前,Shell将始终接受带引号的文件名并删除周围的引号。值得注意的是,这避免了包含空格或任何其他令人讨厌的Shell元字符的文件名的问题。

更新:如果您使用的是Python 3.3或更高版本,请使用shlex.quote而不是编写自己的函数。


8
这就是为什么他会闭合单引号,添加一个转义的单引号,然后再次打开单引号。 - lhunath
4
尽管这并不是 shellquote 函数的责任,但值得注意的是,如果在该函数的返回值之前出现未加引号的反斜杠,则仍会失败。 建议:确保您在可信任的代码中使用此函数(如硬编码命令的一部分),而不要将其附加到其他未加引号的用户输入中。 - lhunath
10
请注意,除非您绝对需要Shell功能,否则您应该使用Jamie的建议。 - lhunath
6
类似于此的功能现在已经正式作为shlex.quote提供。 - Janus Troelsen
3
这个回答中提供的函数比 shlexpipes 更好地处理了 shell 引用(quoting) 。这些 Python 模块错误地假定特殊字符是唯一需要引用的东西,这意味着当不希望出现这种行为时,shell 关键字(如 timecasewhile)将被解析。由于这个原因,我建议使用这个回答中的单引号例程,因为它不试图“聪明地”解决问题,所以没有这些愚蠢的边缘情况。 - user3035772
显示剩余7条评论

65

如果您没有特定的原因使用os.system(),那么您可能应该使用subprocess 模块。您可以直接指定管道并避免使用shell。

下面是来自PEP324的内容:

Replacing shell pipe line
-------------------------

output=`dmesg | grep hda`
==>
p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]

6
subprocess(尤其是check_call等函数)通常比较优秀,但有一些情况下还是需要使用shell escaping。我遇到的主要情况是在调用ssh远端命令时。 - Craig Ringer
@CraigRinger,是的,ssh远程连接是我来到这里的原因。 :P 我希望ssh能在这方面提供一些帮助。 - Jürgen A. Erhard
@JürgenA.Erhard 确实很奇怪它没有 --execvp-remote 选项(或默认情况下不起作用)。通过 shell 做所有事情似乎笨拙而危险。另一方面,ssh 充满了奇怪的怪癖,通常是在狭窄的“安全”视角下完成的,这导致人们想出更不安全的解决方法。 - Craig Ringer

13

或许 subprocess.list2cmdline 更适合?


看起来非常不错。有趣的是,它没有被记录在文档中...(至少在http://docs.python.org/library/subprocess.html中) - Tom
4
它没有正确转义:subprocess.list2cmdline(["'",'',"\\",'"']) 输出为 ' "" \ \" - Tino
它不会转义Shell扩展符号。 - Maxim Razin
subprocess.list2cmdline() 只适用于 Windows 吗? - JS.
@JS 是的,list2cmdline 符合 Windows cmd.exe 语法(请参见 Python 源代码中的函数文档字符串)。shlex.quote 符合 Unix Bourne shell 语法,但通常不是必需的,因为 Unix 直接传递参数有很好的支持。Windows 几乎要求您使用一个包含所有参数的单个字符串(因此需要正确转义)。 - eestrada

6

请注意,Python 2.5和Python 3.1中的pipes.quote实际上是有问题的,不安全可用--它不能处理零长度参数。

>>> from pipes import quote
>>> args = ['arg1', '', 'arg3']
>>> print 'mycommand %s' % (' '.join(quote(arg) for arg in args))
mycommand arg1  arg3

请查看Python问题7476;它已在Python 2.6和3.2及更高版本中得到修复。


4
你使用的Python版本是什么?2.6版本似乎可以产生正确的输出:mycommand arg1 '' arg3(这里有两个单引号连在一起,尽管在Stack Overflow上字体让人较难分辨!) - Brandon Rhodes

4

我认为os.system只是调用了用户配置的任何命令行解释器,所以我不认为您可以以平台无关的方式执行它。我的命令行解释器可能是任何东西,从bash、emacs、ruby,甚至到quake3。这些程序中的一些程序并不期望您传递给它们的参数类型,即使它们能够接受这些参数,也不能保证它们会以相同的方式进行转义。


3
期望一个大多数或完全符合 POSIX 标准的 shell 是合理的(至少在除 Windows 以外的任何地方,而且你知道那时候使用的是哪个“shell”)。os.system 不使用 $SHELL,至少在这里不使用。 - Roger Pate

3

注意: 这是针对 Python 2.7.x 的答案。

根据源码pipes.quote() 是一种“可靠地将字符串引用为 /bin/sh 的单个参数”的方式。(尽管它在版本 2.7 中被弃用,并且在 Python 3.3 中作为 shlex.quote() 函数公开。)

另一方面,在这里subprocess.list2cmdline() 是一种“将一系列参数转换为命令行字符串的方法,使用与 MS C 运行时相同的规则”。

这就是我们以平台无关的方式对命令行中的字符串进行引用的方法。

import sys
mswindows = (sys.platform == "win32")

if mswindows:
    from subprocess import list2cmdline
    quote_args = list2cmdline
else:
    # POSIX
    from pipes import quote

    def quote_args(seq):
        return ' '.join(quote(arg) for arg in seq)

使用方法:

# Quote a single argument
print quote_args(['my argument'])

# Quote multiple arguments
my_args = ['This', 'is', 'my arguments']
print quote_args(my_args)

2
我使用的函数是:
def quote_argument(argument):
    return '"%s"' % (
        argument
        .replace('\\', '\\\\')
        .replace('"', '\\"')
        .replace('$', '\\$')
        .replace('`', '\\`')
    )

也就是说,我总是用双引号将参数括起来,然后在双引号内部反斜杠转义双引号之外的特殊字符。

请注意,您应该使用'\"'、'\$'和'\`',否则转义将不会发生。 - JanKanis
1
此外,在某些(奇怪的)区域设置中使用双引号存在问题,建议的修复方法是使用 pipes.quote,但 @JohnWiseman 指出它也有问题。因此,Greg Hewgill 的答案是要使用的答案。(这也是常规情况下 shell 内部使用的答案。) - mirabilos
谢谢。这对我的情况非常有效。它是Python3.5版本,而shlex没有转义方法,当文本包含$$$时,quote方法也无法帮助。 - Dat TT

0

我认为这些答案对于在Windows上转义命令行参数是个坏主意。根据结果:人们试图应用黑名单方法来过滤“不良”字符,假设(并希望)他们已经全部得到了。Windows非常复杂,未来可能会发现各种各样的字符,可能会允许攻击者劫持命令行参数。

我已经看到一些答案忽略了在Windows中的基本元字符(如分号)。我采取的方法要简单得多:

  1. 制作一个允许的ASCII字符列表。
  2. 删除不在该列表中的所有字符。
  3. 转义斜杠和双引号。
  4. 用双引号括起整个命令,以使命令参数无法被恶意打破和占领。

一个基本的例子:


def win_arg_escape(arg, allow_vars=0):
    allowed_list = """'"/\\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-. """
    if allow_vars:
        allowed_list += "~%$"

    # Filter out anything that isn't a
    # standard character.
    buf = ""
    for ch in arg:
        if ch in allowed_list:
            buf += ch

    # Escape all slashes.
    buf = buf.replace("\\", "\\\\")

    # Escape double quotes.
    buf = buf.replace('"', '""')

    # Surround entire arg with quotes.
    # This avoids spaces breaking a command.
    buf = '"%s"' % (buf)

    return buf


该函数具有启用环境变量和其他shell变量的选项。启用此选项会增加风险,因此默认情况下禁用。

0
在类似于Bash的UNIX shell中,您可以在Python 3中使用shlex.quote来转义shell可能解释的特殊字符,例如空格和*字符:
import os
import shlex

os.system("rm " + shlex.quote(filename))

然而,仅此还不足以保证安全!您仍然需要小心命令参数不被意外解释。例如,如果文件名实际上是像 ../../etc/passwd 这样的路径呢?当您只想删除当前目录下的文件名时,运行 os.system("rm " + shlex.quote(filename)) 可能会删除 /etc/passwd!问题不在于 Shell 解释特殊字符,而是文件名参数并没有被 rm 简单地解释为一个文件名,它实际上被解释为一条路径。

或者,如果有效的文件名以破折号开头,例如 -f 呢?仅传递转义后的文件名是不够的,您需要使用 -- 来禁用选项,或者传递不以破折号开头的路径,例如 ./-f。问题不在于 Shell 解释特殊字符,而是 rm 命令将参数解释为文件名、路径或选项(如果以破折号开头)。

这里是更安全的实现:

if os.sep in filename:
     raise Exception("Did not expect to find file path separator in file name")

os.system("rm -- " + shlex.quote(filename))

我的回答也被删除了:https://i.stack.imgur.com/gSPJD.png - undefined
@FranckDernoncourt 你的回答不应该被删除。 - undefined
他们正在删除所有他们不喜欢的东西,不管SE政策如何:回答评论(发布在https://webapps.meta.stackexchange.com/a/5077/18147,后来被版主取消删除),[聊天消息](https://i.stack.imgur.com/HEyyD.jpg),等等。很乐意通过电子邮件发送更多内容,以免在此评论区混乱,您可以通过franck.dernoncourt@gmail.com与我联系(我们无法使用聊天,因为我被同一版主禁止使用)。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接