vim/0001-patch-9.2.0847-security-vimball-code-execution-via-.patch
2026-09-10 07:23:28 -04:00

272 lines
8.8 KiB
Diff

diff --git a/runtime/autoload/vimball.vim b/runtime/autoload/vimball.vim
index 9c7dcbd..58ac113 100644
--- a/runtime/autoload/vimball.vim
+++ b/runtime/autoload/vimball.vim
@@ -15,9 +15,9 @@ if &cp || exists("g:loaded_vimball")
finish
endif
let g:loaded_vimball = "v37"
-if v:version < 702
+if v:version < 900
echohl WarningMsg
- echo "***warning*** this version of vimball needs vim 7.2"
+ echo "***warning*** this version of vimball needs vim 9.0"
echohl Normal
finish
endif
@@ -268,6 +268,33 @@ fun! vimball#Vimball(really,...)
let filecnt = filecnt + 1
" call Decho("fname<".fname."> fsize=".fsize." filecnt=".filecnt. " fenc=".fenc)
+ " Do not allow a leading /, .. anywhere, or a Windows drive letter
+ " (e.g. C:/foo) in the file name. Backslashes were already converted
+ " to forward slashes above, so this also catches \\server\share UNC
+ " paths via the leading-slash check.
+ if fname =~ '\.\.' || fname =~ '^/' || fname =~ '^\a:'
+ echomsg "(Vimball) Path Traversal Attack detected, aborting..."
+ exe "tabn ".curtabnr
+ bw! Vimball
+ call s:ChgDir(curdir)
+ return
+ " Also, disallow strange paths, that could lead to code execution from
+ " .VimballRecord
+ " Disallow: pipe, quotes and closing paren
+ elseif fname =~ '[|'')"]'
+ echomsg printf("(Vimball) Forbidding strange filename: '%s', aborting...", fname)
+ exe "tabn ".curtabnr
+ 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
echomsg "extracted <".fname.">: ".fsize." lines"
else
@@ -300,13 +327,8 @@ fun! vimball#Vimball(really,...)
" call Decho("dirname<".dirname.">")
" call Decho("dirpath<".dirpath.">")
if !isdirectory(dirname)
-" call Decho("making <".dirname.">")
- if exists("g:vimball_mkdir")
- call system(g:vimball_mkdir." ".shellescape(dirname))
- else
- call mkdir(dirname)
- endif
- call s:RecordInVar(home,"rmdir('".dirname."')")
+ call mkdir(dirname)
+ call s:RecordDirInVar(dirname)
endif
endwhile
endif
@@ -334,13 +356,13 @@ fun! vimball#Vimball(really,...)
if a:really
let fnamepath= home."/".fname
" call Decho("exe w! ".fnameescape(fnamepath))
- if fenc != ""
- exe "silent w! ++enc=".fnameescape(fenc)." ".fnameescape(fnamepath)
- else
- exe "silent w! ".fnameescape(fnamepath)
- endif
- echo "wrote ".fnameescape(fnamepath)
- call s:RecordInVar(home,"call delete('".fnamepath."')")
+ if fenc != ""
+ exe "silent w! ++enc=".fnameescape(fenc)." ".fnameescape(fnamepath)
+ else
+ exe "silent w! ".fnameescape(fnamepath)
+ endif
+ echo "wrote ".fnameescape(fnamepath)
+ call s:RecordInVar(fnamepath)
endif
" return to tab with vimball
@@ -460,12 +482,17 @@ fun! vimball#RmVimball(...)
endif
let s:VBRstring= substitute(exestring,'call delete(','','g')
let s:VBRstring= substitute(s:VBRstring,"[')]",'','g')
-" call Decho("exe ".exestring)
- sil! keepalt keepjumps exe exestring
- sil! keepalt keepjumps d
- let exestring= strlen(substitute(exestring,'call delete(.\{-})|\=',"D","g"))
-" call Decho("exestring<".exestring.">")
- echomsg "removed ".exestring." files"
+ 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
+ echomsg "removed ".nr_files." files"
else
let s:VBRstring= ''
let curfile = substitute(curfile,'\.vmb','','')
@@ -599,21 +626,20 @@ fun! s:ChgDir(newdir)
endfun
" ---------------------------------------------------------------------
-" s:RecordInVar: record a un-vimball command in the .VimballRecord file {{{2
-fun! s:RecordInVar(home,cmd)
-" call Dfunc("RecordInVar(home<".a:home."> cmd<".a:cmd.">)")
- if a:cmd =~ '^rmdir'
-" if !exists("s:recorddir")
-" let s:recorddir= substitute(a:cmd,'^rmdir',"call s:Rmdir",'')
-" else
-" let s:recorddir= s:recorddir."|".substitute(a:cmd,'^rmdir',"call s:Rmdir",'')
-" endif
- elseif !exists("s:recordfile")
- let s:recordfile= a:cmd
- else
- let s:recordfile= s:recordfile."|".a: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=[]
+ 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 Dret("RecordInVar : s:recordfile<".(exists("s:recordfile")? s:recordfile : "")."> s:recorddir<".(exists("s:recorddir")? s:recorddir : "").">")
+ call add(s:recorddir, $'call delete({string(a:dir)},"d")')
endfun
" ---------------------------------------------------------------------
@@ -637,11 +663,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
" call Dret("s:RecordInFile : neither recordfile nor recorddir exist")
return
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index 3d9e7fa..8e46eea 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -232,6 +232,7 @@ NEW_TESTS = \
test_plugin_phpcomplete \
test_plugin_python3complete \
test_plugin_tar \
+ test_plugin_vimball \
test_plus_arg_edit \
test_popup \
test_popupwin \
@@ -486,6 +487,7 @@ NEW_TESTS_RES = \
test_plugin_phpcomplete.res \
test_plugin_python3complete.res \
test_plugin_tar.res \
+ test_plugin_vimball.res \
test_plus_arg_edit.res \
test_popup.res \
test_popupwin.res \
diff --git a/src/testdir/test_plugin_vimball.vim b/src/testdir/test_plugin_vimball.vim
new file mode 100644
index 0000000..1c68bf2
--- /dev/null
+++ b/src/testdir/test_plugin_vimball.vim
@@ -0,0 +1,84 @@
+" Test for the vimball plugin
+
+let s:testdir = expand("<script>:h")
+let s:default_vimball = ['" Vimball Archiver by Charles E. Campbell',
+ \ 'UseVimball',
+ \ 'finish',
+ \ 'XVimball/Xtest.txt [[[1',
+ \ '2',
+ \ 'Hello Vimball',
+ \ '123']
+
+func SetUp()
+ ru plugin/vimballPlugin.vim
+ let g:vimball_home = s:testdir
+endfunc
+
+func TearDown()
+ call delete('Xtest.vmb')
+ call delete('.VimballRecord')
+endfunc
+
+func s:setup()
+ call mkdir('XVimball', 'p')
+ call writefile(['Hello Vimball', '123'], 'XVimball/Xtest.txt')
+endfunc
+
+func s:teardown()
+ call delete('XVimball', 'rf')
+ call delete('Xtest.vmb')
+ bw! Xtest.vmb
+ bw! XVimball/Xtest.txt
+ if bufloaded('.VimballRecord')
+ bw! .VimballRecord
+ endif
+endfunc
+
+func s:Mkvimball()
+ call s:setup()
+ new
+ 0put ='XVimball/Xtest.txt'
+ $d
+ 1,1MkVimball! Xtest
+ bw!
+endfunc
+
+func Test_vimball_basic()
+ call s:Mkvimball()
+ call assert_true(filereadable('Xtest.vmb'), 'vimball file should be created')
+ call assert_equal(s:default_vimball, readfile('Xtest.vmb'))
+
+ call delete('XVimball', 'rf')
+ sp Xtest.vmb
+ let mess = execute(':mess')
+ call assert_match('\*\*\*vimball\*\*\* Source this file to extract it!', mess)
+ so %
+ call feedkeys("\<cr>", "t")
+ unlet mess
+ let mess = execute(':mess')->split('\n')
+ call assert_equal('extracted <XVimball/Xtest.txt>: 2 lines', mess[-2])
+
+ call assert_true(filereadable('XVimball/Xtest.txt'), 'extracted file should exist')
+ call assert_equal(['Hello Vimball', '123'], readfile('XVimball/Xtest.txt'))
+
+ " Vimball extraction has been recorded
+ call assert_true(filereadable('.VimballRecord'))
+ let record = readfile('.VimballRecord')
+ call assert_equal(1, record->len())
+ call assert_match('^Xtest.vmb: call delete(''.\{-}'')|call delete(''.\{-}'',"d")$', record[0])
+ 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