CVE-2019-20807: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: In Vim before 8.1.0881, users can circumvent the rvim restricted mode and execute arbitrary OS commands via scripting interfaces (e.g., Python, Ruby, or Lua).
CWE description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
CVSS v3 base score: 5.3
Published: 2020-05-28T14:15Z
Repository: https://github.com/vim/vim
Language: C
Files changed: runtime/doc/starting.txt, src/evalfunc.c, src/ex_cmds.c, src/ex_docmd.c, src/if_perl.xs, src/testdir/Make_all.mak, src/testdir/test_restricted.vim, src/version.c
Commit date: 2019-02-08 14:34:10 +0100
Commit message: patch 8.1.0881: can execute shell commands in rvim through interfaces

Problem:    Can execute shell commands in rvim through interfaces.
Solution:   Disable using interfaces in restricted mode. Allow for writing
            file with writefile(), histadd() and a few others.

Diff: diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt
index 711a4876b6..6289e9c8f7 100644
--- a/runtime/doc/starting.txt
+++ b/runtime/doc/starting.txt
@@ -246,16 +246,22 @@ a slash.  Thus "-R" means recovery and "-/R" readonly.
 		will be reset, so that writing files is disabled.  However,
 		the 'modifiable' and 'write' options can be set to enable
 		changes and writing.
 		{not in Vi}
 
-						*-Z* *restricted-mode* *E145*
+					*-Z* *restricted-mode* *E145* *E981*
 -Z		Restricted mode.  All commands that make use of an external
 		shell are disabled.  This includes suspending with CTRL-Z,
-		":sh", filtering, the system() function, backtick expansion,
-		delete(), rename(), mkdir(), writefile(), libcall(),
-		job_start(), etc.
+		":sh", filtering, the system() function, backtick expansion
+		and libcall().
+		Also disallowed are delete(), rename(), mkdir(), job_start(),
+		etc.
+		Interfaces, such as Python, Ruby and Lua, are also disabled,
+		since they could be used to execute shell commands.  Perl uses
+		the Safe module.
+		Note that the user may still find a loophole to execute a
+		shell command, it has only been made difficult.
 		{not in Vi}
 
 							*-g*
 -g		Start Vim in GUI mode.  See |gui|. For the opposite see |-v|.
 		{not in Vi}
diff --git a/src/evalfunc.c b/src/evalfunc.c
index fa7ed9bab1..eb082b771b 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -6815,11 +6815,11 @@ f_histadd(typval_T *argvars UNUSED, typval_T *rettv)
     char_u	*str;
     char_u	buf[NUMBUFLEN];
 #endif
 
     rettv->vval.v_number = FALSE;
-    if (check_restricted() || check_secure())
+    if (check_secure())
 	return;
 #ifdef FEAT_CMDHIST
     str = tv_get_string_chk(&argvars[0]);	/* NULL on type error */
     histype = str != NULL ? get_histtype(str) : -1;
     if (histype >= 0)
@@ -7896,10 +7896,13 @@ f_log10(typval_T *argvars, typval_T *rettv)
 f_luaeval(typval_T *argvars, typval_T *rettv)
 {
     char_u	*str;
     char_u	buf[NUMBUFLEN];
 
+    if (check_restricted() || check_secure())
+	return;
+
     str = tv_get_string_buf(&argvars[0], buf);
     do_luaeval(str, argvars + 1, rettv);
 }
 #endif
 
@@ -8642,10 +8645,12 @@ f_mode(typval_T *argvars, typval_T *rettv)
 f_mzeval(typval_T *argvars, typval_T *rettv)
 {
     char_u	*str;
     char_u	buf[NUMBUFLEN];
 
+    if (check_restricted() || check_secure())
+	return;
     str = tv_get_string_buf(&argvars[0], buf);
     do_mzeval(str, rettv);
 }
 
     void
@@ -8930,10 +8935,13 @@ f_pumvisible(typval_T *argvars UNUSED, typval_T *rettv UNUSED)
 f_py3eval(typval_T *argvars, typval_T *rettv)
 {
     char_u	*str;
     char_u	buf[NUMBUFLEN];
 
+    if (check_restricted() || check_secure())
+	return;
+
     if (p_pyx == 0)
 	p_pyx = 3;
 
     str = tv_get_string_buf(&argvars[0], buf);
     do_py3eval(str, rettv);
@@ -8948,10 +8956,13 @@ f_py3eval(typval_T *argvars, typval_T *rettv)
 f_pyeval(typval_T *argvars, typval_T *rettv)
 {
     char_u	*str;
     char_u	buf[NUMBUFLEN];
 
+    if (check_restricted() || check_secure())
+	return;
+
     if (p_pyx == 0)
 	p_pyx = 2;
 
     str = tv_get_string_buf(&argvars[0], buf);
     do_pyeval(str, rettv);
@@ -8963,10 +8974,13 @@ f_pyeval(typval_T *argvars, typval_T *rettv)
  * "pyxeval()" function
  */
     static void
 f_pyxeval(typval_T *argvars, typval_T *rettv)
 {
+    if (check_restricted() || check_secure())
+	return;
+
 # if defined(FEAT_PYTHON) && defined(FEAT_PYTHON3)
     init_pyxversion();
     if (p_pyx == 2)
 	f_pyeval(argvars, rettv);
     else
@@ -10817,11 +10831,11 @@ f_setbufvar(typval_T *argvars, typval_T *rettv UNUSED)
     buf_T	*buf;
     char_u	*varname, *bufvarname;
     typval_T	*varp;
     char_u	nbuf[NUMBUFLEN];
 
-    if (check_restricted() || check_secure())
+    if (check_secure())
 	return;
     (void)tv_get_number(&argvars[0]);	    /* issue errmsg if type error */
     varname = tv_get_string_chk(&argvars[1]);
     buf = tv_get_buf(&argvars[0], FALSE);
     varp = &argvars[2];
@@ -11339,11 +11353,11 @@ f_settabvar(typval_T *argvars, typval_T *rettv)
     char_u	*varname, *tabvarname;
     typval_T	*varp;
 
     rettv->vval.v_number = 0;
 
-    if (check_restricted() || check_secure())
+    if (check_secure())
 	return;
 
     tp = find_tabpage((int)tv_get_number_chk(&argvars[0], NULL));
     varname = tv_get_string_chk(&argvars[1]);
     varp = &argvars[2];
@@ -14712,11 +14726,11 @@ f_writefile(typval_T *argvars, typval_T *rettv)
     listitem_T	*li;
     list_T	*list = NULL;
     blob_T	*blob = NULL;
 
     rettv->vval.v_number = -1;
-    if (check_restricted() || check_secure())
+    if (check_secure())
 	return;
 
     if (argvars[0].v_type == VAR_LIST)
     {
 	list = argvars[0].vval.v_list;
diff --git a/src/ex_cmds.c b/src/ex_cmds.c
index a3974c1bad..681ef422a9 100644
--- a/src/ex_cmds.c
+++ b/src/ex_cmds.c
@@ -4773,11 +4773,11 @@ ex_z(exarg_T *eap)
     int
 check_restricted(void)
 {
     if (restricted)
     {
-	emsg(_("E145: Shell commands not allowed in rvim"));
+	emsg(_("E145: Shell commands and some functionality not allowed in rvim"));
 	return TRUE;
     }
     return FALSE;
 }
 
diff --git a/src/ex_docmd.c b/src/ex_docmd.c
index b90ea7b17c..ccca2f98fa 100644
--- a/src/ex_docmd.c
+++ b/src/ex_docmd.c
@@ -2005,15 +2005,20 @@ do_one_cmd(
     if (!ea.skip)
     {
 #ifdef HAVE_SANDBOX
 	if (sandbox != 0 && !(ea.argt & SBOXOK))
 	{
-	    /* Command not allowed in sandbox. */
+	    // Command not allowed in sandbox.
 	    errormsg = _(e_sandbox);
 	    goto doend;
 	}
 #endif
+	if (restricted != 0 && (ea.argt & RESTRICT))
+	{
+	    errormsg = _("E981: Command not allowed in rvim");
+	    goto doend;
+	}
 	if (!curbuf->b_p_ma && (ea.argt & MODIFY))
 	{
 	    /* Command not allowed in non-'modifiable' buffer */
 	    errormsg = _(e_modifiable);
 	    goto doend;
diff --git a/src/if_perl.xs b/src/if_perl.xs
index 203bb6a679..67d0b94888 100644
--- a/src/if_perl.xs
+++ b/src/if_perl.xs
@@ -969,10 +969,11 @@ VIM_init(void)
 }
 
 #ifdef DYNAMIC_PERL
 static char *e_noperl = N_("Sorry, this command is disabled: the Perl library could not be loaded.");
 #endif
+static char *e_perlsandbox = N_("E299: Perl evaluation forbidden in sandbox without the Safe module");
 
 /*
  * ":perl"
  */
     void
@@ -1017,17 +1018,16 @@ ex_perl(exarg_T *eap)
     {
 	sv = newSVpv(script, 0);
 	vim_free(script);
     }
 
-#ifdef HAVE_SANDBOX
-    if (sandbox)
+    if (sandbox || secure)
     {
 	safe = perl_get_sv("VIM::safe", FALSE);
 # ifndef MAKE_TEST  /* avoid a warning for unreachable code */
 	if (safe == NULL || !SvTRUE(safe))
-	    emsg(_("E299: Perl evaluation forbidden in sandbox without the Safe module"));
+	    emsg(_(e_perlsandbox));
 	else
 # endif
 	{
 	    PUSHMARK(SP);
 	    XPUSHs(safe);
@@ -1035,11 +1035,10 @@ ex_perl(exarg_T *eap)
 	    PUTBACK;
 	    perl_call_method("reval", G_DISCARD);
 	}
     }
     else
-#endif
 	perl_eval_sv(sv, G_DISCARD | G_NOARGS);
 
     SvREFCNT_dec(sv);
 
     err = SvPV(GvSV(PL_errgv), length);
@@ -1296,17 +1295,16 @@ do_perleval(char_u *str, typval_T *rettv)
     {
 	dSP;
 	ENTER;
 	SAVETMPS;
 
-#ifdef HAVE_SANDBOX
-	if (sandbox)
+	if (sandbox || secure)
 	{
 	    safe = get_sv("VIM::safe", FALSE);
 # ifndef MAKE_TEST  /* avoid a warning for unreachable code */
 	    if (safe == NULL || !SvTRUE(safe))
-		emsg(_("E299: Perl evaluation forbidden in sandbox without the Safe module"));
+		emsg(_(e_perlsandbox));
 	    else
 # endif
 	    {
 		sv = newSVpv((char *)str, 0);
 		PUSHMARK(SP);
@@ -1318,11 +1316,10 @@ do_perleval(char_u *str, typval_T *rettv)
 		SvREFCNT_dec(sv);
 		sv = POPs;
 	    }
 	}
 	else
-#endif /* HAVE_SANDBOX */
 	    sv = eval_pv((char *)str, 0);
 
 	if (sv) {
 	    perl_to_vim(sv, rettv);
 	    ref_map_free();
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index 5857a228f5..2ca5f2b1de 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -211,10 +211,11 @@ NEW_TESTS = \
 	test_regex_char_classes \
 	test_regexp_latin \
 	test_regexp_utf8 \
 	test_registers \
 	test_reltime \
+	test_restricted \
 	test_retab \
 	test_ruby \
 	test_scriptnames \
 	test_scroll_opt \
 	test_scrollbind \
@@ -373,10 +374,11 @@ NEW_TESTS_RES = \
 	test_pyx3.res \
 	test_quickfix.res \
 	test_quotestar.res \
 	test_regex_char_classes.res \
 	test_registers.res \
+	test_restricted.res \
 	test_retab.res \
 	test_ruby.res \
 	test_scriptnames.res \
 	test_scrollbind.res \
 	test_search.res \
diff --git a/src/testdir/test_restricted.vim b/src/testdir/test_restricted.vim
new file mode 100644
index 0000000000..9dd937cf3e
--- /dev/null
+++ b/src/testdir/test_restricted.vim
@@ -0,0 +1,107 @@
+" Test for "rvim" or "vim -Z"
+
+source shared.vim
+
+func Test_restricted()
+  let cmd = GetVimCommand('Xrestricted')
+  if cmd == ''
+    return
+  endif
+
+  call writefile([
+	\ "silent !ls",
+	\ "call writefile([v:errmsg], 'Xrestrout')",
+	\ "qa!",
+	\ ], 'Xrestricted')
+  call system(cmd . ' -Z')
+  call assert_match('E145:', join(readfile('Xrestrout')))
+
+  call delete('Xrestricted')
+  call delete('Xrestrout')
+endfunc
+
+func Run_restricted_test(ex_cmd, error)
+  let cmd = GetVimCommand('Xrestricted')
+  if cmd == ''
+    return
+  endif
+
+  call writefile([
+	\ a:ex_cmd,
+	\ "call writefile([v:errmsg], 'Xrestrout')",
+	\ "qa!",
+	\ ], 'Xrestricted')
+  call system(cmd . ' -Z')
+  call assert_match(a:error, join(readfile('Xrestrout')))
+
+  call delete('Xrestricted')
+  call delete('Xrestrout')
+endfunc
+
+func Test_restricted_lua()
+  if !has('lua')
+    throw 'Skipped: Lua is not supported'
+  endif
+  call Run_restricted_test('lua print("Hello, Vim!")', 'E981:')
+  call Run_restricted_test('luado return "hello"', 'E981:')
+  call Run_restricted_test('luafile somefile', 'E981:')
+  call Run_restricted_test('call luaeval("expression")', 'E145:')
+endfunc
+
+func Test_restricted_mzscheme()
+  if !has('mzscheme')
+    throw 'Skipped: MzScheme is not supported'
+  endif
+  call Run_restricted_test('mzscheme statement', 'E981:')
+  call Run_restricted_test('mzfile somefile', 'E981:')
+  call Run_restricted_test('call mzeval("expression")', 'E145:')
+endfunc
+
+func Test_restricted_perl()
+  if !has('perl')
+    throw 'Skipped: Perl is not supported'
+  endif
+  " TODO: how to make Safe mode fail?
+  " call Run_restricted_test('perl system("ls")', 'E981:')
+  " call Run_restricted_test('perldo system("hello")', 'E981:')
+  " call Run_restricted_test('perlfile somefile', 'E981:')
+  " call Run_restricted_test('call perleval("system(\"ls\")")', 'E145:')
+endfunc
+
+func Test_restricted_python()
+  if !has('python')
+    throw 'Skipped: Python is not supported'
+  endif
+  call Run_restricted_test('python print "hello"', 'E981:')
+  call Run_restricted_test('pydo return "hello"', 'E981:')
+  call Run_restricted_test('pyfile somefile', 'E981:')
+  call Run_restricted_test('call pyeval("expression")', 'E145:')
+endfunc
+
+func Test_restricted_python3()
+  if !has('python3')
+    throw 'Skipped: Python3 is not supported'
+  endif
+  call Run_restricted_test('py3 print "hello"', 'E981:')
+  call Run_restricted_test('py3do return "hello"', 'E981:')
+  call Run_restricted_test('py3file somefile', 'E981:')
+  call Run_restricted_test('call py3eval("expression")', 'E145:')
+endfunc
+
+func Test_restricted_ruby()
+  if !has('ruby')
+    throw 'Skipped: Ruby is not supported'
+  endif
+  call Run_restricted_test('ruby print "Hello"', 'E981:')
+  call Run_restricted_test('rubydo print "Hello"', 'E981:')
+  call Run_restricted_test('rubyfile somefile', 'E981:')
+endfunc
+
+func Test_restricted_tcl()
+  if !has('tcl')
+    throw 'Skipped: Tcl is not supported'
+  endif
+  call Run_restricted_test('tcl puts "Hello"', 'E981:')
+  call Run_restricted_test('tcldo puts "Hello"', 'E981:')
+  call Run_restricted_test('tclfile somefile', 'E981:')
+endfunc
diff --git a/src/version.c b/src/version.c
index 1235cf0eda..48236fe354 100644
--- a/src/version.c
+++ b/src/version.c
@@ -781,10 +781,12 @@ static char *(features[]) =
 	NULL
 };
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    881,
 /**/
     880,
 /**/
     879,
 /**/

Vulnerable code: 						*-Z* *restricted-mode* *E145*
		":sh", filtering, the system() function, backtick expansion,
		delete(), rename(), mkdir(), writefile(), libcall(),
		job_start(), etc.
    if (check_restricted() || check_secure())
    if (check_restricted() || check_secure())
    if (check_restricted() || check_secure())
    if (check_restricted() || check_secure())
	emsg(_("E145: Shell commands not allowed in rvim"));
	    /* Command not allowed in sandbox. */
#ifdef HAVE_SANDBOX
    if (sandbox)
	    emsg(_("E299: Perl evaluation forbidden in sandbox without the Safe module"));
#endif
#ifdef HAVE_SANDBOX
	if (sandbox)
		emsg(_("E299: Perl evaluation forbidden in sandbox without the Safe module"));
#endif /* HAVE_SANDBOX */
Fixed code: 					*-Z* *restricted-mode* *E145* *E981*
		":sh", filtering, the system() function, backtick expansion
		and libcall().
		Also disallowed are delete(), rename(), mkdir(), job_start(),
		etc.
		Interfaces, such as Python, Ruby and Lua, are also disabled,
		since they could be used to execute shell commands.  Perl uses
		the Safe module.
		Note that the user may still find a loophole to execute a
		shell command, it has only been made difficult.
    if (check_secure())
    if (check_restricted() || check_secure())
	return;

    if (check_restricted() || check_secure())
	return;
    if (check_restricted() || check_secure())
	return;

    if (check_restricted() || check_secure())
	return;

    if (check_restricted() || check_secure())
	return;

    if (check_secure())
    if (check_secure())
    if (check_secure())
	emsg(_("E145: Shell commands and some functionality not allowed in rvim"));
	    // Command not allowed in sandbox.
	if (restricted != 0 && (ea.argt & RESTRICT))
	{
	    errormsg = _("E981: Command not allowed in rvim");
	    goto doend;
	}
static char *e_perlsandbox = N_("E299: Perl evaluation forbidden in sandbox without the Safe module");
    if (sandbox || secure)
	    emsg(_(e_perlsandbox));
	if (sandbox || secure)
		emsg(_(e_perlsandbox));
	test_restricted \
	test_restricted.res \
" Test for "rvim" or "vim -Z"

source shared.vim

func Test_restricted()
  let cmd = GetVimCommand('Xrestricted')
  if cmd == ''
    return
  endif

  call writefile([
	\ "silent !ls",
	\ "call writefile([v:errmsg], 'Xrestrout')",
	\ "qa!",
	\ ], 'Xrestricted')
  call system(cmd . ' -Z')
  call assert_match('E145:', join(readfile('Xrestrout')))

  call delete('Xrestricted')
  call delete('Xrestrout')
endfunc

func Run_restricted_test(ex_cmd, error)
  let cmd = GetVimCommand('Xrestricted')
  if cmd == ''
    return
  endif

  call writefile([
	\ a:ex_cmd,
	\ "call writefile([v:errmsg], 'Xrestrout')",
	\ "qa!",
	\ ], 'Xrestricted')
  call system(cmd . ' -Z')
  call assert_match(a:error, join(readfile('Xrestrout')))

  call delete('Xrestricted')
  call delete('Xrestrout')
endfunc

func Test_restricted_lua()
  if !has('lua')
    throw 'Skipped: Lua is not supported'
  endif
  call Run_restricted_test('lua print("Hello, Vim!")', 'E981:')
  call Run_restricted_test('luado return "hello"', 'E981:')
  call Run_restricted_test('luafile somefile', 'E981:')
  call Run_restricted_test('call luaeval("expression")', 'E145:')
endfunc

func Test_restricted_mzscheme()
  if !has('mzscheme')
    throw 'Skipped: MzScheme is not supported'
  endif
  call Run_restricted_test('mzscheme statement', 'E981:')
  call Run_restricted_test('mzfile somefile', 'E981:')
  call Run_restricted_test('call mzeval("expression")', 'E145:')
endfunc

func Test_restricted_perl()
  if !has('perl')
    throw 'Skipped: Perl is not supported'
  endif
  " TODO: how to make Safe mode fail?
  " call Run_restricted_test('perl system("ls")', 'E981:')
  " call Run_restricted_test('perldo system("hello")', 'E981:')
  " call Run_restricted_test('perlfile somefile', 'E981:')
  " call Run_restricted_test('call perleval("system(\"ls\")")', 'E145:')
endfunc

func Test_restricted_python()
  if !has('python')
    throw 'Skipped: Python is not supported'
  endif
  call Run_restricted_test('python print "hello"', 'E981:')
  call Run_restricted_test('pydo return "hello"', 'E981:')
  call Run_restricted_test('pyfile somefile', 'E981:')
  call Run_restricted_test('call pyeval("expression")', 'E145:')
endfunc

func Test_restricted_python3()
  if !has('python3')
    throw 'Skipped: Python3 is not supported'
  endif
  call Run_restricted_test('py3 print "hello"', 'E981:')
  call Run_restricted_test('py3do return "hello"', 'E981:')
  call Run_restricted_test('py3file somefile', 'E981:')
  call Run_restricted_test('call py3eval("expression")', 'E145:')
endfunc

func Test_restricted_ruby()
  if !has('ruby')
    throw 'Skipped: Ruby is not supported'
  endif
  call Run_restricted_test('ruby print "Hello"', 'E981:')
  call Run_restricted_test('rubydo print "Hello"', 'E981:')
  call Run_restricted_test('rubyfile somefile', 'E981:')
endfunc

func Test_restricted_tcl()
  if !has('tcl')
    throw 'Skipped: Tcl is not supported'
  endif
  call Run_restricted_test('tcl puts "Hello"', 'E981:')
  call Run_restricted_test('tcldo puts "Hello"', 'E981:')
  call Run_restricted_test('tclfile somefile', 'E981:')
endfunc
/**/
    881,