$ 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
?
8 Answers 8
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
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"
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.
-
See my answer for a portable version of this.R.. GitHub STOP HELPING ICE– R.. GitHub STOP HELPING ICE2018年05月07日 16:07:16 +00:00Commented 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 whereprintf
is not builtin. Most shells haveprintf
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 bybash
. Actually,bash
'sprintf
fails to quote things properly for even itself for some values of$myvar
in some locales.Stéphane Chazelas– Stéphane Chazelas2018年05月07日 20:45:08 +00:00Commented May 7, 2018 at 20:45 -
With
bash-4.3
at least, that's still a command injection vulnerability withmyvar=$'\xa3``reboot`\xa3`'
in a zh_HK.big5hkscs locale for instance.Stéphane Chazelas– Stéphane Chazelas2018年05月07日 21:03:59 +00:00Commented May 7, 2018 at 21:03
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")"
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
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"
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
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\""
-
2Still a command injection vulnerability, like for values of
$myvar
like$(reboot)
or"; reboot; #
Stéphane Chazelas– Stéphane Chazelas2018年05月07日 11:59:16 +00:00Commented May 7, 2018 at 11:59 -
1using
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.Stéphane Chazelas– Stéphane Chazelas2018年05月07日 12:04:06 +00:00Commented May 7, 2018 at 12:04
cd
only has effect inside thebash -c
shell.sudo bash -c
I have a variable expansion to a pathname which might contains whitespaces.