Escape arguments for paramiko.SSHClient().exec_command
What is the best way to escape a string for safe usage as a command-line argument? I know that using subprocess.Popen
takes care of this using list2cmdline()
, but that doesn't seem to work correctly for paramiko. Example:
from subprocess import Popen
Popen(['touch', 'foo;uptime']).wait()
This creates a file named literally foo;uptime
, which is what I want. Compare:
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()
This creates a file called foo
and prints the uptime of the remote host. It has executed uptime
as a second command instead of using it as part of the argument to the first command, touch
. This is not what I want.
I tried escaping the semicolon with a backslash before and after sending it to list2cmdline
, but then I ended up with a file called foo\;uptime
.
Also, it works correctly if instead of uptime
, you use a command with a space:
stdin, stdout, stderr = ssh.exec_command(list2cmdline(['touch', 'foo;echo test']))
print stdout.read()
This creates a file literally called foo;echo test
because list2cmdline
surrounded it with quotes.
Also, I tried pipes.quote开发者_运维知识库()
and it had the same effect as list2cmdline
.
EDIT: To clarify, I need to make sure that only a single command gets executed on the remote host, regardless of the whatever input data I receive, which means escaping characters like ;
, &
, and the backtick.
Assuming the remote user has a POSIX shell, this should work:
def shell_escape(arg):
return "'%s'" % (arg.replace(r"'", r"'\''"), )
Why does this work?
POSIX shell single quotes are defined as:
Enclosing characters in single-quotes ( '' ) shall preserve the literal value of each character within the single-quotes. A single-quote cannot occur within single-quotes.
The idea here is that you enclose the string in single quotes. This, alone, is almost good enough --- every character except a single quote will be interpreted literally. For single quotes, you drop out of the single-quoted string (the first '
), add a single quote (the \'
), and then resume the single quoted string (the last '
).
What does this work with?
This should work for any POSIX shell. I've tested it with dash and bash. Solaris 5.10's /bin/sh
(which I believe is not POSIX-compatible, and I couldn't find a spec for) also seems to work.
For arbitrary remote hosts, I believe this is impossible. I think ssh
will execute your command with whatever the remote user's shell (as configured in /etc/passwd
or equivalent). If the remote user might be running, say, /usr/bin/python
or git-shell
or something, not only is any quoting scheme probably going to run into cross-shell inconsistencies, but you command execution is probably going to fail too.
csh / tcsh
Slightly more problematic is the possibility that the remote user might be running tcsh
, since some people actually do run that in the wild and might expect paramiko's exec_command
to work. (Users of /usr/bin/python
as a shell probably have no such expectations...)
tcsh seems to mostly work. However, I can't figure out a way to quote a newline such that it will be happy. Including a newline in single-quoted string seems to make tcsh unhappy:
$ tcsh -c $'echo \'foo\nbar\''
Unmatched '.
Unmatched '.
Other than newlines, everything I've tried seems to work with tcsh (including single quotes, double quotes, backslashes, embedded tabs, asterisks, ...).
Testing shell escaping
If you have an escaping scheme, here are some things you might want to test with:
- Escape sequences (
\n
,\t
, ...) - Quotes (
'
,"
,\
) - Globbing characters (
*
,?
,[]
, etc.) - Job control and pipelines (
|
,&
,||
,&&
, ...) - Newlines
Newlines are worth a special note. The re.escape
solution doesn't handle this right --- it escapes any non-alphanumeric character, and POSIX shell considers an escaped newline (ie, in Python, the two-letter string "\\\n"
) to be zero characters, not a single newline character. I think re.escape
handles all other cases correctly, though it scares me to use something designed for regular expressions to do escaping for shell. It might turn out to work, but I'd worry about a subtle case in re.escape
or shell escaping rules (like newlines), or possible future changes in the API.
You should also be aware that escape sequences can get processed at various stages, which complicates testing things --- you only care about what the shell passes to a program, not what the program does. Using printf "%s\n" escaped-string-to-test
is probably the best bet. echo
works surprisingly poorly: In dash, the echo
built-in processes backslash escapes like \n
. Using /bin/echo
is usually safe, but on a Solaris 5.10 machine I tested on, it also handles sequences like \n
.
You are not having success with list2cmdline()
because it targets the Microsoft command line, which has different rules than the POSIX command line with which you are communicating using SSH.
Instead, use the native Python routine pipes.quote()
, and be careful to apply it separately to each argument in the command. This will give you a working command line for SSH:
from pipes import quote
command = ['touch', 'foo;uptime']
print ' '.join(quote(s) for s in command)
The output carefully quotes the second argument to protect the ;
character:
touch 'foo;uptime'
re.escape()
is what I am looking for.
re.*escape(**string*)
Return **string* with all non-alphanumerics backslashed...
Example:
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')]))
This creates a file on the server called foo;uptime
, which is what I want.
I have tried all of the shell meta-characters I can think of and it works :
stdin, stdout, stderr = ssh.exec_command(' '.join(['touch', re.escape('test;rm foo&echo "Uptime: `uptime`"')]))
精彩评论