]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0847: [security]: vimball: code execution via .VimballRecord file v9.2.0847
authorChristian Brabandt <cb@256bit.org>
Fri, 24 Jul 2026 15:43:51 +0000 (17:43 +0200)
committerChristian Brabandt <cb@256bit.org>
Fri, 24 Jul 2026 19:19:25 +0000 (19:19 +0000)
Problem:  [security]: vimball: code execution via .VimballRecord file
          (tdjackey)
Solution: Forbid arbitrary commands, fix broken directory deletion code,
          refactor code

Github Security Advisory:
https://github.com/vim/vim/security/advisories/GHSA-r22p-fhw4-84p2

Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/autoload/vimball.vim
src/testdir/test_plugin_vimball.vim
src/version.c

index a1b2e7ffcef012ded243c62def0613b84e28d5e3..d4ea66cd1ef6c36dac50c059c3b6e239294b4c54 100644 (file)
@@ -1,7 +1,7 @@
 " vimball.vim : construct a file containing both paths and files
 " Maintainer: This runtime file is looking for a new maintainer.
 " Original Author:     Charles E. Campbell
-" Date:                        Jun 29, 2026
+" Date:                        Jul 23, 2026
 " Version:     37 (with modifications from the Vim Project)
 " GetLatestVimScripts: 1502 1 :AutoInstall: vimball.vim
 " Copyright: (c) 2004-2011 by Charles E. Campbell
@@ -16,9 +16,9 @@ if &cp || exists("g:loaded_vimball")
  finish
 endif
 let g:loaded_vimball = "v37"
-if v:version < 704
+if v:version < 900
  echohl WarningMsg
- echo "***warning*** this version of vimball needs vim 7.4"
+ echo "***warning*** this version of vimball needs vim 9.0"
  echohl Normal
  finish
 endif
@@ -245,6 +245,12 @@ fun! vimball#Vimball(really,...)
      bw! Vimball
      call s:ChgDir(curdir)
      return
+   elseif fname =~? '\%(^\|/\)\.VimballRecord$'
+     echomsg "(Vimball) Forbidding .VimballRecord filename, aborting..."
+     exe "tabn ".curtabnr
+     bw! Vimball
+     call s:ChgDir(curdir)
+     return
    endif
 
    if a:really
@@ -272,7 +278,7 @@ fun! vimball#Vimball(really,...)
      let fnamebuf = substitute(fnamebuf,'^.\{-}/\(.*\)$','\1','')
      if !isdirectory(dirname)
       call mkdir(dirname)
-      call s:RecordInVar(home,"rmdir('".dirname."')")
+      call s:RecordDirInVar(dirname)
      endif
     endwhile
    endif
@@ -303,7 +309,7 @@ fun! vimball#Vimball(really,...)
       exe "silent w! ".fnameescape(fnamepath)
     endif
     echo "wrote ".fnameescape(fnamepath)
-    call s:RecordInVar(home,"call delete('".escape(fnamepath, '"''|')."')")
+    call s:RecordInVar(fnamepath)
     endif
 
     " return to tab with vimball
@@ -402,10 +408,17 @@ fun! vimball#RmVimball(...)
     endif
     let s:VBRstring= substitute(exestring,'call delete(','','g')
     let s:VBRstring= substitute(s:VBRstring,"[')]",'','g')
-    sil! keepalt keepjumps exe exestring
+    let nr_files= 0
+    for line in split(exestring, '|')
+      if line !~ '^call delete(''[^'']\{-}''\(,"d"\)\?)$'
+        echomsg "ignoring .VimballRecord entry: " line
+      else
+        sil! keepalt keepjumps exe line
+        let nr_files+= 1
+      endif
+    endfor
     sil! keepalt keepjumps d
-    let exestring= strlen(substitute(exestring,'call delete(.\{-})|\=',"D","g"))
-    echomsg "removed ".exestring." files"
+    echomsg "removed ".nr_files." files"
    else
     let s:VBRstring= ''
     let curfile    = substitute(curfile,'\.vmb','','')
@@ -547,13 +560,20 @@ fun! s:ChgDir(newdir)
 endfun
 
 " ---------------------------------------------------------------------
-" s:RecordInVar: record a un-vimball command in the .VimballRecord file {{{2
-fun! s:RecordInVar(home,cmd)
+" s:RecordInVar: record a un-vimball file deletion in the .VimballRecord file {{{2
+fun! s:RecordInVar(file)
   if !exists("s:recordfile")
-   let s:recordfile= a:cmd
-  else
-   let s:recordfile= s:recordfile."|".a:cmd
+    let s:recordfile=[]
+  endif
+  call add(s:recordfile, $'call delete({string(a:file)})')
+endfun
+
+" s:RecordDirInVar: record a un-vimball dir deletion in the .VimballRecord file {{{2
+fun! s:RecordDirInVar(dir)
+  if !exists("s:recorddir")
+    let s:recorddir = []
   endif
+  call add(s:recorddir, $'call delete({string(a:dir)},"d")')
 endfun
 
 " ---------------------------------------------------------------------
@@ -574,11 +594,11 @@ fun! s:RecordInFile(home)
    setlocal ma
    $
    if exists("s:recordfile") && exists("s:recorddir")
-    let cmd= cmd.s:recordfile."|".s:recorddir
+    let cmd= cmd.join(s:recordfile, '|')."|".join(s:recorddir, '|')
    elseif exists("s:recorddir")
-    let cmd= cmd.s:recorddir
+    let cmd= cmd.join(s:recorddir, '|')
    elseif exists("s:recordfile")
-    let cmd= cmd.s:recordfile
+    let cmd= cmd.join(s:recordfile, '|')
    else
     return
    endif
index 30093ef719c02138941062d19861c0e824063e4b..156b4c8433b20e81dd13d7b358090e70c2b9e6a7 100644 (file)
@@ -65,7 +65,7 @@ func Test_vimball_basic()
   call assert_true(filereadable('.VimballRecord'))
   let record = readfile('.VimballRecord')
   call assert_equal(1, record->len())
-  call assert_match('^Xtest.vmb: rmdir.*call delete(', record[0])
+  call assert_match('^Xtest.vmb: call delete(''.\{-}'')|call delete(''.\{-}'',"d")$', record[0])
   call s:teardown()
 endfunc
 
@@ -110,3 +110,17 @@ func Test_vimball_evil_filenames()
   call assert_match('(Vimball) Forbidding strange filename:.* aborting\.\.\.', mess)
   call s:teardown()
 endfunc
+
+func Test_vimball_VimballRecord_filenames()
+  call s:Mkvimball()
+  call delete('XVimball', 'rf')
+  sp Xtest.vmb
+  4s#.*\ze\t#.VimballRecord#
+  so %
+  call feedkeys("\<cr>", "it")
+
+  let mess = execute(':mess')->split('\n')[-1]
+  call assert_match('(Vimball) Forbidding .VimballRecord filename.* aborting\.\.\.', mess)
+  call assert_false(filereadable('.VimballRecord'))
+  call s:teardown()
+endfunc
index 5547cf4d524ad7ac46750c68bfa4c2756a850f49..a0b96e2ebbc50c331d4f2c141981bb6ffd054e2c 100644 (file)
@@ -758,6 +758,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    847,
 /**/
     846,
 /**/