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'
865k204 gold badges1.8k silver badges2.3k bronze badges
asked May 7, 2018 at 11:48
2
  • 4
    No matter how you solve this, it still won't do very much. The cd only has effect inside the bash -c shell. Commented May 7, 2018 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 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. Commented May 7, 2018 at 12:20

8 Answers 8

14

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!1

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

1 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

answered May 7, 2018 at 11:56
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"
answered May 7, 2018 at 22:19
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.

answered May 7, 2018 at 16:03
3
  • See my answer for a portable version of this. Commented May 7, 2018 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. Commented May 7, 2018 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. Commented May 7, 2018 at 21:03
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")"
answered May 7, 2018 at 16:06
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
answered May 7, 2018 at 20:31
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"
answered May 8, 2018 at 12:37
0
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
answered Aug 9, 2020 at 19:12
-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\""
answered May 7, 2018 at 11:58
2
  • 2
    Still a command injection vulnerability, like for values of $myvar like $(reboot) or "; reboot; # Commented May 7, 2018 at 11:59
  • 1
    using sudo bash -c "cd -P -- '$myvar'" would limit the problem to only values of $myvar that contain single quote characters which would be easier to sanitize. Commented May 7, 2018 at 12:04

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.