chiark / gitweb /
SSH quoting
authorColin Watson <cjwatson@debian.org>
Fri, 11 Jun 2021 10:34:35 +0000 (11:34 +0100)
committerColin Watson <cjwatson@debian.org>
Fri, 11 Jun 2021 10:34:35 +0000 (11:34 +0100)
content/ssh-quoting.md [new file with mode: 0644]

diff --git a/content/ssh-quoting.md b/content/ssh-quoting.md
new file mode 100644 (file)
index 0000000..0626c09
--- /dev/null
@@ -0,0 +1,104 @@
+Title: SSH quoting
+Slug: ssh-quoting
+Date: 2021-06-11 11:22:21 +0100
+Category: debian
+Tags: openssh, planet-debian, planet-ubuntu
+
+A while back there was a thread on one of our company mailing lists about
+SSH quoting, and I posted a long answer to it.  Since then a few people have
+asked me questions that caused me to reach for it, so I thought it might be
+helpful if I were to anonymize the original question and post my answer
+here.
+
+The question was why a sequence of commands involving `ssh` and fiddly
+quoting produced the output they did.  The first example was this:
+
+    $ ssh user@machine.local bash -lc "cd /tmp;pwd"
+    /home/user
+
+Oh hi, my dubious life choices have been such that this is my specialist
+subject!
+
+This is because SSH command-line parsing is not quite what you expect.
+
+First, recall that your local shell will apply its usual parsing, and the
+actual OS-level execution of `ssh` will be like this:
+
+    [0]: ssh
+    [1]: user@machine.local
+    [2]: bash
+    [3]: -lc
+    [4]: cd /tmp;pwd
+
+Now, the SSH wire protocol only takes a single string as the command, with
+the expectation that it should be passed to a shell by the remote end.  The
+OpenSSH client deals with this by taking all its arguments after things like
+options and the target, which in this case are:
+
+    [0]: bash
+    [1]: -lc
+    [2]: cd /tmp;pwd
+
+It then joins them with a single space:
+
+    bash -lc cd /tmp;pwd
+
+This is passed as a string to the server, which then passes that entire
+string to a shell for evaluation, so as if you'd typed this directly on the
+server:
+
+    sh -c 'bash -lc cd /tmp;pwd'
+
+The shell then parses this as two commands:
+
+    bash -lc cd /tmp
+    pwd
+
+The directory change thus happens in a subshell (actually it doesn't quite
+even do that, because `bash -lc cd /tmp` in fact ends up just calling `cd`
+because of the way `bash -c` parses multiple arguments), and then that
+subshell exits, then `pwd` is called in the outer shell which still has the
+original working directory.
+
+The second example was this:
+
+    $ ssh user@machine.local bash -lc
+    "pwd;cd /tmp;pwd"
+    /home/user
+    /tmp
+
+Following the logic above, this ends up as if you'd run this on the server:
+
+    sh -c 'bash -lc pwd; cd /tmp; pwd'
+
+The third example was this:
+
+    $ ssh user@machine.local bash -lc "cd
+    /tmp;cd /tmp;pwd"
+    /tmp
+
+And this is as if you'd run:
+
+    sh -c 'bash -lc cd /tmp; cd /tmp; pwd'
+
+Now, I wouldn't have implemented the SSH client this way, because I agree
+that it's confusing.  But `/usr/bin/ssh` is used as a transport for other
+things so much that changing its behaviour now would be enormously
+disruptive, so it's probably impossible to fix.  (I have occasionally
+agitated on openssh-unix-dev@ for at least documenting this better, but
+haven't made much headway yet; I need to get round to preparing a
+documentation patch.)  Once you know about it you can use the proper
+quoting, though.  In this case that would simply be:
+
+    ssh user@machine.local 'cd /tmp;pwd'
+
+Or if you do need to specifically invoke `bash -l` there for some reason
+(I'm assuming that the original example was reduced from something more
+complicated), then you can minimise your confusion by passing the whole
+thing as a single string in the form you want the remote `sh -c` to see, in
+a way that ensures that the quotes are preserved and sent to the server
+rather than being removed by your local shell:
+
+    ssh user@machine.local 'bash -lc "cd /tmp;pwd"'
+
+Shell parsing is hard.