From aa514d298762e54da33d3d4e9b4763f27a667639 Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Fri, 11 Jun 2021 11:34:35 +0100 Subject: [PATCH] SSH quoting --- content/ssh-quoting.md | 104 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 content/ssh-quoting.md diff --git a/content/ssh-quoting.md b/content/ssh-quoting.md new file mode 100644 index 00000000..0626c096 --- /dev/null +++ b/content/ssh-quoting.md @@ -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. -- 2.30.2