From: Christian Brabandt Date: Fri, 24 Jul 2026 15:43:51 +0000 (+0200) Subject: patch 9.2.0847: [security]: vimball: code execution via .VimballRecord file X-Git-Tag: v9.2.0847^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=581a2f3ac9c6f96a26324f6b2c8c11415fd0d452;p=thirdparty%2Fvim.git patch 9.2.0847: [security]: vimball: code execution via .VimballRecord file 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 --- diff --git a/runtime/autoload/vimball.vim b/runtime/autoload/vimball.vim index a1b2e7ffce..d4ea66cd1e 100644 --- a/runtime/autoload/vimball.vim +++ b/runtime/autoload/vimball.vim @@ -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 diff --git a/src/testdir/test_plugin_vimball.vim b/src/testdir/test_plugin_vimball.vim index 30093ef719..156b4c8433 100644 --- a/src/testdir/test_plugin_vimball.vim +++ b/src/testdir/test_plugin_vimball.vim @@ -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("\", "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 diff --git a/src/version.c b/src/version.c index 5547cf4d52..a0b96e2ebb 100644 --- a/src/version.c +++ b/src/version.c @@ -758,6 +758,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 847, /**/ 846, /**/