3

The shell has a great feature, where it'll preserve argument quoting across variable expansion when you use "$@", such that the script:

for f in "$@"; do echo "$f"; done

when invoked with arguments:

"with spaces" '$and $(metachars)'

will print, literally:

with spaces
$and $(metachars)

This isn't the normal behaviour of expansion of a quoted string, it seems to be a special case for "$@".

Is there any way to get this behaviour for other variables? In the specific case I'm interested in, I want to safely expand $SSH_ORIGINAL_COMMAND in a command= specifier in a restricted public key entry, without having to worry about spaces in arguments, metacharacters, etc.

"$SSH_ORIGINAL_COMMAND" expands like "$*" would, i.e. a naïve expansion that doesn't add any quoting around separate arguments.

Is the information required for "$@" style expansion simply not available to the shell in this case, by the time it gets the env var SSH_ORIGINAL_COMMAND? So I'd instead need to convince sshd to quote the arguments?

The answer to this question is making me wonder if it's possible at all.

Community
  • 1
  • 1
Craig Ringer
  • 307,061
  • 76
  • 688
  • 778
  • Craig, the issue you are running into is one of word-splitting I believe. While a brute force solution may involve setting `IFS` to control splitting, I am not clear enough on what exactly is failing to offer any type of answer. In your effort to get `$SSH_ORIGINAL_COMMAND in a command=`, what is the content of `$SSH_ORIGINAL_COMMAND` and what results in the `command=` public key entry? – David C. Rankin Jul 08 '14 at 06:30
  • 1
    I think the issue is that the information required is lost before it reaches bash. sshd would have to inject a bash array, instead of just supply an environment variable that's a flat string. Or it'd have to pre-quote the contents of the env var. I'll edit and follow up in a bit. – Craig Ringer Jul 08 '14 at 07:16
  • That is what prompted my thought of `IFS`. Let's say you have your `$SSH_ORIGINAL_COMMAND` that contains '-x -p portno other stuff` and for some reason, that isn't working with the `command=`. If only part of the commands properly reach `command=`, you could set `IFS=$'\n'` to only break on newlines and insure `command=` receives the total from `$SSH_ORIGINAL_COMMAND`. I'm not sure if that fits the situation, but that was what I was able to get from the description – David C. Rankin Jul 08 '14 at 08:05

1 Answers1

4

You can get similar "quoted dollar-at" behavior for arbitrary arrays using "${YOUR_ARRAY_HERE[@]}" syntax for bash arrays. Of course, that's no complete answer, because you still have to break the string into multiple array elements according to the quotes.

One thought was to use bash -x, which renders expanded output, but only if you actually run the command; it doesn't work with -n, which prevents you from actually executing the commands in question. Likewise you could use eval or bash -c along with set -- to manage the quote removal, performing expansion on the outer shell and quote removal on the inner shell, but that would be extremely hard to bulletproof against executing arbitrary code.

As an end run, use xargs instead. xargs handles single and double quotes. This is a very imperfect solution, because xargs treats backslash-escaped characters very differently than bash does and fails entirely to handle semicolons and so forth, but if your input is relatively predictable it gets you most of the way there without forcing you to write a full shell parser.

SSH_ORIGINAL_COMMAND='foo     "bar  baz"  $quux'

# Build out the parsed array.
# Bash 4 users may be able to do this with readarray or mapfile instead.
# You may also choose to null-terminate if newlines matter.
COMMAND_ARRAY=()
while read line; do
  COMMAND_ARRAY+=("$line")
done < <(xargs -n 1 <<< "$SSH_ORIGINAL_COMMAND")

# Demonstrate working with the array.
N=0
for arg in "${COMMAND_ARRAY[@]}"; do
  echo "COMMAND_ARRAY[$N]: $arg"
  ((N++))
done

Output:

COMMAND_ARRAY[0]: foo
COMMAND_ARRAY[1]: bar  baz
COMMAND_ARRAY[2]: $quux
Community
  • 1
  • 1
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251