Assertion failed

Apr 30, 2015

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.

posted at 16:00  ·   ·  sh  bash  getopt  linux  bsd
Mastodon