AdmiralNemo AdmiralNemo - 1 year ago 456
Python Question

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

takes care of this using
, 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
, 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']))

This creates a file called
and prints the uptime of the remote host. It has executed
as a second command instead of using it as part of the argument to the first command,
. This is not what I want.

I tried escaping the semicolon with a backslash before and after sending it to
, but then I ended up with a file called

Also, it works correctly if instead of
, you use a command with a space:

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

This creates a file literally called
foo;echo test
surrounded it with quotes.

Also, I tried
and it had the same effect as

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.

Answer Source

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.