CVE-2019-20807 (published 2020-05-28T14:15Z)

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

Severity: MEDIUM
CVSS v2 base score: 4.6 (AV:L/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L)

Weakness information:
cwe_id: CWE-78
cwe_name: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
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.
extended_description: 
		   This weakness can lead to a vulnerability in environments in which the attacker does not have direct access to the operating system, such as in web applications. Alternately, if the weakness occurs in a privileged program, it could allow the attacker to specify commands that normally would not be accessible, or to call alternate commands with privileges that the attacker does not have. The problem is exacerbated if the compromised process does not follow the principle of least privilege, because the attacker-controlled commands may run with special system privileges that increases the amount of damage.
            There are at least two subtypes of OS command injection:
               
				 The application intends to execute a single, fixed program that is under its own control. It intends to use externally-supplied inputs as arguments to that program. For example, the program might use system("nslookup [HOSTNAME]") to run nslookup and allow the user to supply a HOSTNAME, which is used as an argument. Attackers cannot prevent nslookup from executing. However, if the program does not remove command separators from the HOSTNAME argument, attackers could place the separators into the arguments, which allows them to execute their own program after nslookup has finished executing.
				 The application accepts an input that it uses to fully select which program to run, as well as which commands to use. The application simply redirects this entire command to the operating system. For example, the program might use "exec([COMMAND])" to execute the [COMMAND] that was supplied by the user. If the COMMAND is under attacker control, then the attacker can execute arbitrary commands or programs. If the command is being executed using functions like exec() and CreateProcess(), the attacker might not be able to combine multiple commands together in the same line.
               
            From a weakness standpoint, these variants represent distinct programmer errors. In the first variant, the programmer clearly intends that input from untrusted parties will be part of the arguments in the command to be executed. In the second variant, the programmer does not intend for the command to be accessible to any untrusted party, but the programmer probably has not accounted for alternate ways in which malicious attackers can provide input.
         
         

Fix information:
repo_url: https://github.com/vim/vim
commit_details: repo_url: https://github.com/vim/vim
author: Bram Moolenaar
author_date: 2019-02-08 14:34:10+01:00
msg: 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.
repository: repo_url: https://github.com/vim/vim
repo_name: vim/vim
description: The official Vim repository
repo_language: Vim Script
owner: vim
file_changes: filename: starting.txt
change_type: ModificationType.MODIFY
diff: @@ -248,12 +248,18 @@ a slash.  Thus "-R" means recovery and "-/R" readonly.
 		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*

programming_language: Perl
filename: evalfunc.c
change_type: ModificationType.MODIFY
diff: @@ -6817,7 +6817,7 @@ f_histadd(typval_T *argvars UNUSED, typval_T *rettv)
 #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 */
@@ -7898,6 +7898,9 @@ 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);
 }
@@ -8644,6 +8647,8 @@ 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);
 }
@@ -8932,6 +8937,9 @@ 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;
 
@@ -8950,6 +8958,9 @@ 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;
 
@@ -8965,6 +8976,9 @@ f_pyeval(typval_T *argvars, typval_T *rettv)
     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)
@@ -10819,7 +10833,7 @@ f_setbufvar(typval_T *argvars, typval_T *rettv UNUSED)
     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]);
@@ -11341,7 +11355,7 @@ f_settabvar(typval_T *argvars, typval_T *rettv)
 
     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));
@@ -14714,7 +14728,7 @@ f_writefile(typval_T *argvars, typval_T *rettv)
     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)

programming_language: C
filename: ex_cmds.c
change_type: ModificationType.MODIFY
diff: @@ -4775,7 +4775,7 @@ 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;

programming_language: C
filename: ex_docmd.c
change_type: ModificationType.MODIFY
diff: @@ -2007,11 +2007,16 @@ do_one_cmd(
 #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 */

programming_language: Shell
filename: if_perl.xs
change_type: ModificationType.MODIFY
diff: @@ -971,6 +971,7 @@ 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"
@@ -1019,13 +1020,12 @@ ex_perl(exarg_T *eap)
 	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
 	{
@@ -1037,7 +1037,6 @@ ex_perl(exarg_T *eap)
 	}
     }
     else
-#endif
 	perl_eval_sv(sv, G_DISCARD | G_NOARGS);
 
     SvREFCNT_dec(sv);
@@ -1298,13 +1297,12 @@ do_perleval(char_u *str, typval_T *rettv)
 	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
 	    {
@@ -1320,7 +1318,6 @@ do_perleval(char_u *str, typval_T *rettv)
 	    }
 	}
 	else
-#endif /* HAVE_SANDBOX */
 	    sv = eval_pv((char *)str, 0);
 
 	if (sv) {

programming_language: Perl
filename: Make_all.mak
change_type: ModificationType.MODIFY
diff: @@ -213,6 +213,7 @@ NEW_TESTS = \
 	test_regexp_utf8 \
 	test_registers \
 	test_reltime \
+	test_restricted \
 	test_retab \
 	test_ruby \
 	test_scriptnames \
@@ -375,6 +376,7 @@ NEW_TESTS_RES = \
 	test_quotestar.res \
 	test_regex_char_classes.res \
 	test_registers.res \
+	test_restricted.res \
 	test_retab.res \
 	test_ruby.res \
 	test_scriptnames.res \

programming_language: Shell
filename: test_restricted.vim
change_type: ModificationType.ADD
diff: @@ -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

programming_language: Lua
filename: version.c
change_type: ModificationType.MODIFY
diff: @@ -783,6 +783,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    881,
 /**/
     880,
 /**/

programming_language: Ruby