10
$ myvar="/path to/my directory"
$ sudo bash -c "cd $myvar"

In such case, how can I quote $myvar to avoid word splitting because of the white spaces in the value of myvar?

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
Tim
  • 98,580
  • 191
  • 570
  • 977
  • 4
    No matter how you solve this, it still won't do very much. The `cd` only has effect inside the `bash -c` shell. – Kusalananda May 07 '18 at 12:16
  • I am giving only a minimal example for the question only, it is not a working solution to my real problem, which is https://unix.stackexchange.com/a/269080/674 plus that in the command string given to `sudo bash -c` I have a variable expansion to a pathname which might contains whitespaces. – Tim May 07 '18 at 12:20

8 Answers8

13

There's no word splitting (as in the feature that splits variables upon unquoted expansions) in that code as $myvar is not unquoted.

There is however a command injection vulnerability as $myvar is expanded before being passed to bash. So its content is interpreted as bash code!

Spaces in there will cause several arguments to be passed to cd, not because of word splitting, but because they will be parsed as several tokens in the shell syntax. With a value of bye;reboot, that will reboot!¹

Here, you'd want:

sudo bash -c 'cd -P -- "$1"' bash "$myvar"

(where you pass the contents of $myvar as the first argument of that inline script; note how both $myvar and $1 were quoted for their respective shell to prevent IFS-word-splitting (and globbing)).

Or:

sudo MYVAR="$myvar" bash -c 'cd -P -- "$MYVAR"'

(where you pass the contents of $myvar in an environment variable).

Of course you won't achieve anything useful by running only cd in that inline script (other than checking whether root can cd into there). Presumably, you want that script to cd there and then do something else there like:

sudo bash -c 'cd -P -- "$1" && do-something' bash "$myvar"

If the intention was to use sudo to be able to cd into a directory which you otherwise don't have access to, then that cannot really work.

sudo sh -c 'cd -P -- "$1" && exec bash' sh "$myvar"

will start an interactive bash with its current directory in $myvar. But that shell will be running as root.

You could do:

sudo sh -c 'cd -P -- "$1" && exec sudo -u "$SUDO_USER" bash' sh "$myvar"

To get an unprivileged interactive bash with the current directory being $myvar, but if you didn't have the permissions to cd into that directory in the first place, you won't be able to do anything in that directory even if it's your current working directory.

$ myvar=/var/spool/cron/crontabs
$ sudo sh -c 'cd -P -- "$1" && exec sudo -u "$SUDO_USER" bash' sh "$myvar"
bash-4.4$ ls
ls: cannot open directory '.': Permission denied

An exception would be if you do have search permission to the directory itself but not to one of the directory components of its path:

$ myvar=1/2
$ mkdir -p "$myvar"
$ chmod 0 1
$ cd 1/2
cd: permission denied: 1/2
$ sudo sh -c 'cd -P -- "$1" && exec sudo -u "$SUDO_USER" bash' sh "$myvar"
bash-4.4$ pwd
/home/stephane/1/2
bash-4.4$ mkdir 3
bash-4.4$ ls
3
bash-4.4$ cd "$PWD"
bash: cd: /home/stephane/1/2: Permission denied

¹ strictly speaking, for values of $myvar like $(seq 10) (literally), there would be word splitting of course upon expansion of that command substitution by the bash shell started as root

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
4

If you can't trust the contents of $myvar, the only reasonable approach is to use GNU printf to create an escaped version of it:

#!/bin/bash

myvar=$(printf '%q' "$myvar")
bash -c "echo $myvar"

As the printf man page says:

%q

ARGUMENT is printed in a format that can be reused as shell input, escaping non-printable characters with the proposed POSIX $'' syntax.

Toby Speight
  • 8,460
  • 3
  • 26
  • 50
  • See my answer for a portable version of this. – R.. GitHub STOP HELPING ICE May 07 '18 at 16:07
  • GNU `printf` will only be invoked there on GNU systems and if that `$(printf '%q' "$myvar")` is run in a shell where `printf` is not builtin. Most shells have `printf` builtin nowadays. Not all support a `%q` though and when they do, they would quote thing in a format supported by the corresponding shell, which may not necessarily be the same as that understood by `bash`. Actually, `bash`'s `printf` fails to quote things properly for even itself for some values of `$myvar` in some locales. – Stéphane Chazelas May 07 '18 at 20:45
  • With `bash-4.3` at least, that's still a command injection vulnerability with `myvar=$'\xa3\`\`reboot\`\xa3\`'` in a zh_HK.big5hkscs locale for instance. – Stéphane Chazelas May 07 '18 at 21:03
4

There is new (bash 4.4) quoting magic that lets you more directly do what you want. Depending on the larger context, using one of the other techniques might be better, but it does work in this limited context.

sudo bash -c "cd ${myvar@Q}; pwd"
Seth Robertson
  • 351
  • 1
  • 2
  • 9
3

First of all, as others have noted your cd command is useless since it happens in the context of a shell that immediately exits. But in general the question makes sense. What you need is a way to shell quote an arbitrary string, so that it can be used in a context where it will be interpreted as a shell-quoted string. I have an example of this in my sh tricks page:

quote () { printf %s\\n "$1" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/" ; }

With this function you can then do:

myvar="/path to/my directory"
sudo bash -c "cd $(quote "$myvar")"
3

What Stéphane Chazelas suggests is definitely the best way to do this. I'll just provide an answer on how to safely quote with parameter expansion syntax as opposed to using a sed subprocess, like in R..'s answer.

myvar='foo \'\''"bar'

quote() {
  printf "'%s'" "${1//\'/\'\\\'\'}"
}

printf "value: %s\n" "$myvar"
printf "quoted: %s\n" "$(quote "$myvar")"
bash -c "printf 'interpolated in subshell script: %s\n' $(quote "$myvar")"

Output of that script is:

value: foo \'"bar
quoted: 'foo \'\''"bar'
interpolated in subshell script: foo \'"bar
JoL
  • 4,520
  • 15
  • 35
1

Yes, there is word splitting.
Well, technically, command line splitting on bash parsing the line.

Let's first get quoting right:

The simplest (and insecure) of solutions to get the command to work as I believe you will want it to is:

bash -c "cd \"$myvar\""

Let's confirm what happens (assuming myvar="/path to/my directory"):

$ bash -c "printf '<%s>\n' $myvar"
<./path>
<to/my>
<directory>

As you can see, the path string was split on spaces. And:

$ bash -c "printf '<%s>\n' \"$myvar\""
<./path to/my directory>

Is not split.

However, the command (as written) is insecure. Better use:

$ bash -c "cd -P -- \"$myvar\"; pwd"
/home/user/temp/path to/my directory

That will protect cd against files that start with a dash (-) or following links that may lead to problems.

That will work if you can trust that the string in $myvar is a path.
However, "code injection" is still possible as you are "executing an string":

$ myvar='./path to/my directory";date;"true'
$ bash -c "printf '<%s> ' \"$myvar\"; echo"
<./path to/my directory> Tue May  8 12:25:51 UTC 2018

You need an stronger quoting of the string, like:

$ bash -c 'printf "<%s> " "$1"; echo' _ "$myvar"
<./path to/my directory";date -u;"true>

As you can see above, the value was not interpreted but just used as "$1'.

Now we can (safely) use sudo:

$ sudo bash -c 'cd -P -- "$1"; pwd' _ "$myvar"
_: line 0: cd: ./path to/my directory";date -u;"true: No such file or directory

If there are several arguments, you may use:

$ sudo bash -c 'printf "<%s>" "$@"' _ "$val1" "$val2" "$val3"
1

IFS does not affect the readin split, only expansion splits, so you could just shut wordsplitting off at the execution layer rather than play quoting games to dance around it.


To get a sudo'd subshell doing this, pass the var into its environment (note the single quotes here):

myvar=$HOME/'my quirky path'
myvar=$myvar sudo -E bash -c 'IFS=; cd $myvar; pwd'

or

sudo myvar="$myvar" bash -c 'IFS=; cd $myvar;pwd'

but it's not usually this clunky, when you're not trying to punch through multiple layers the shell syntax for this is more reasonable, and seeing the IFS assignment up front quiets the urge to obsessively inspect for quoting safety:

(IFS=; sudo frob $myfile)

or

myfunc() { local IFS=; sudo frob $1; }

The shell never wordsplits variable assignments, you don't have to quote anything in

date=`date` somecommand
jthill
  • 2,671
  • 12
  • 15
-2

Single quotes won't work here, since then your variable won't be expanded. If you are confident that the variable for your path is sanitized, the simplest solution is to simply add quotes around the resulting expansion, once it is in the new bash shell:

sudo bash -c "cd \"$myvar\""
cunninghamp3
  • 621
  • 7
  • 17