为paramiko.SSHClient().exec_command转义参数

6 投票
3 回答
9592 浏览
提问于 2025-04-16 00:43

怎样才能安全地处理字符串,以便在命令行参数中使用呢?我知道使用 subprocess.Popen 可以通过 list2cmdline() 来解决这个问题,但这在使用 paramiko 时似乎不太管用。举个例子:

from subprocess import Popen
Popen(['touch', 'foo;uptime']).wait()

这个命令会创建一个名为 foo;uptime 的文件,这正是我想要的。对比一下:

from paramiko import SSHClient()
from subprocess import list2cmdline
ssh = SSHClient()
#... load host keys and connect to a server
stdin, stdout, stderr = ssh.exec_command(list2cmdline(['touch', 'foo;uptime']))
print stdout.read()

这个命令创建了一个叫 foo 的文件,并且打印了远程主机的运行时间。它把 uptime 当作第二个命令执行了,而不是把它当作第一个命令 touch 的一部分。这不是我想要的结果。

我尝试在发送给 list2cmdline 之前,用反斜杠转义分号,但结果是我得到了一个叫 foo\;uptime 的文件。

另外,如果用一个带空格的命令代替 uptime,它就能正常工作:

stdin, stdout, stderr = ssh.exec_command(list2cmdline(['touch', 'foo;echo test']))
print stdout.read()

这个命令会创建一个名为 foo;echo test 的文件,因为 list2cmdline 用引号把它包裹起来了。

我还尝试了 pipes.quote(),结果和 list2cmdline 一样。

补充说明:我需要确保在远程主机上只执行一个命令,无论我收到什么输入数据,这就意味着需要转义像 ;& 和反引号这样的字符。

3 个回答

-3

我正在寻找的是re.escape()这个功能。

re.*escape(**字符串*)

这个功能会把**字符串*中的所有非字母数字字符前面加上反斜杠...

举个例子:

from paramiko import SSHClient()
from subprocess import list2cmdline
import re
ssh = SSHClient()
#... load host keys and connect to a server
stdin, stdout, stderr = ssh.exec_command(' '.join(['touch', re.escape('foo;uptime')]))

这段代码在服务器上创建了一个叫做foo;uptime的文件,这正是我想要的。

我尝试了我能想到的所有shell元字符,它都能正常工作:

stdin, stdout, stderr = ssh.exec_command(' '.join(['touch', re.escape('test;rm foo&echo "Uptime: `uptime`"')]))
1

你在使用 list2cmdline() 时遇到问题,是因为它是针对微软的命令行设计的,而微软的命令行规则和你通过SSH使用的POSIX命令行规则不一样。

你可以使用Python自带的 pipes.quote() 方法,记得要对命令中的每个参数单独使用它。这样你就能得到一个适用于SSH的命令行:

from pipes import quote
command = ['touch', 'foo;uptime']
print ' '.join(quote(s) for s in command)

输出中小心地对第二个参数进行了引号处理,以保护 ; 字符:

touch 'foo;uptime'
11

假设远程用户使用的是POSIX shell,这个方法应该可以用:

def shell_escape(arg):
    return "'%s'" % (arg.replace(r"'", r"'\''"), )

为什么这样可以?

POSIX shell中的单引号是这样定义的:

用单引号('')包裹的字符会保持每个字符的字面值。单引号不能出现在单引号内部。

这里的意思是,你把字符串放在单引号里面。这样做几乎就足够了——除了单引号以外的每个字符都会被直接理解。对于单引号,你需要先退出单引号字符串(第一个'),然后添加一个单引号(\'),最后再继续单引号字符串(最后一个')。

这个方法适用于什么?

这个方法应该适用于任何POSIX shell。我在dash和bash上测试过。Solaris 5.10的/bin/sh(我认为它不兼容POSIX,我也找不到相关规范)似乎也可以用。

对于任意的远程主机,我认为这是不可能的。我觉得ssh会用远程用户的shell来执行你的命令(这个shell在/etc/passwd或类似文件中配置)。如果远程用户可能在运行,比如说/usr/bin/python或者git-shell之类的,不仅任何引用方案可能会遇到跨shell的不一致性,而且你的命令执行可能也会失败。

csh / tcsh

稍微复杂一点的是,远程用户可能在使用tcsh,因为确实有些人会在实际环境中使用它,并且可能会期待paramiko的exec_command能正常工作。(使用/usr/bin/python作为shell的用户可能没有这样的期待...)

tcsh似乎大部分情况下都能工作。不过,我找不到一种方法可以让它开心地处理换行符。在单引号字符串中包含换行符似乎会让tcsh不高兴:

$ tcsh -c $'echo \'foo\nbar\''
Unmatched '.
Unmatched '.

除了换行符,我尝试的其他所有内容在tcsh中似乎都能正常工作(包括单引号、双引号、反斜杠、嵌入的制表符、星号等)。

测试shell转义

如果你有一个转义方案,这里有一些你可能想测试的内容:

  • 转义序列(\n, \t, ...)
  • 引号(', ", \
  • 通配符字符(*, ?, [], 等等)
  • 作业控制和管道(|, &, ||, &&, ...)
  • 换行符

换行符值得特别注意。re.escape的解决方案处理得不太对——它会转义任何非字母数字字符,而POSIX shell认为转义的换行符(也就是在Python中,两个字符的字符串"\\\n")是零个字符,而不是一个换行符。我认为re.escape能正确处理其他所有情况,尽管我用一个为正则表达式设计的东西来处理shell转义让我有点担心。它可能会有效,但我会担心re.escape或shell转义规则(比如换行符)中的某些细微情况,或者未来API的变化。

你还应该知道,转义序列可能在不同阶段被处理,这让测试变得复杂——你只关心shell传递给程序的内容,而不是程序如何处理这些内容。使用printf "%s\n" escaped-string-to-test可能是最好的选择。echo的表现出乎意料地差:在dash中,echo内置命令会处理反斜杠转义,比如\n。使用/bin/echo通常是安全的,但在我测试的Solaris 5.10机器上,它也会处理像\n这样的序列。

撰写回答