Getopt from bad to worse
Note: This is about getopt in shells e.g. bash, not languages like C
Getopt used to be the way to parse arguments in shellscripts, but it
has some issues. Most notably it’s very hard to handle whitespaces in
arguments correctly. Let’s say you have the flag -f
which takes some
value e.g. -f myval
. If this value contains a whitespace, e.g. -f
'my val'
, we have a problem. Getting this parsed correctly in the
shell is really hard and I can’t remember ever having seen it done in
a way without notable issues. Because of this modern shells contain a
function called getopts
, which solves most of the problems getopt has.
If all this has already been solved why are we here? Well, after
searching online for how to parse long options with getopts which it
usualy doesn’t support natively, I came across alot of posts where
they used getopt instead since at least the GNU version has support
for long options. In an effort I belive is to get around the
whitespace issue they will send the output from getopt through eval
.
Let’s look at some examples. Usually you will see something similar to the following when getopt is used.
opts=$(getopt $optstring "$@")
set -- $opts
But in a lot of the code out there it’s done like this instead.
opts=$(getopt $optstring "$@")
eval set -- "$opts"
With the way the shell works this doesn’t solve any problems with whitespaces, instead it introduces new challenges, which will often let users execute commands in the context of the script.
As an example let’s use the following script as an example.
#!/bin/sh
opts=$(getopt 'f:' "$@")
eval set -- "$opts"
f_arg=
while true ; do
case "$1" in
-f)
f_arg="$2"
shift 2
;;
--)
shift
break
;;
*)
echo "Something unexpected: $1" >&1
shift
;;
esac
done
echo "$f_arg"
Now let’s see how it behaves.
% ./getopt.sh
% ./getopt.sh -f
getopt: option requires an argument -- 'f'
% ./getopt.sh -f test
test
% ./getopt.sh -f 'test1 test2'
Something unexpected: test2
test1
At first glance this doesn’t look right, but this is expected
behavior, even when quoting $opts
and using eval. This is not the
most interesting part and you hopefully knew this already, so let’s
continue and try some more creative arguments.
% ./getopt.sh -f 'test1 -- ;echo test2 #'
test2
test1
What’s happening here we are able to specify commands to be executed in the context of the script as commandline arguments. In most cases it would be harmless, but I would argue it’s still not a good thing. The bad big issue arises when the script is used by unprivieleged users through for example sudo to interact with some piece of software. E.g. you might have some servers on which some users need to interact with a piece of software. They might need to read out information from the software, and to do this they need to be a special user. To get around this you allow them to run these commands/script through sudo, in which case this issue could have a noteworthy security impact by allowing the user to run arbitrary commands instead of just a few pre-defined ones.
Let’s see this example in action where we have to run the script as a
specific user. /usr/local/bin/getopt.sh
is the same script as
before, but modified to abort as early as possible if running as the
wrong user.
% whoami
marius
% which getopt.sh
/usr/local/bin/getopt.sh
% getopt.sh -f test
Not the correct user!
% sudo -l
User marius may run the followig commands on testhost:
(svcuser) /usr/local/bin/getopt.sh
% sudo -u svcuser getopt.sh -f test
test
% sudo -u svcuser getopt.sh -f 'test -- ;whoami #'
svcuser
test
% sudo -u svcuser getopt.sh -f 'test -- ;sh ;exit #'
$ whoami
svcuser
$ exit
With the last example where we started a shell the original program
will stop executing after the shell we spawn exits thanks to the
exit
in the arguments.
In the end I would recomend using getopts
instead of getopt
, as it
solves the whitespace problem and it’s built into most shells today,
and if you for some reason have to use getopt you should not send
its output through eval.