From: Christian Brabandt Date: Tue, 15 Jul 2025 19:54:00 +0000 (+0200) Subject: patch 9.1.1552: [security]: path traversal issue in tar.vim X-Git-Tag: v9.1.1552^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=87757c6b0a4b2c1f71c72ea8e1438b8fb116b239;p=thirdparty%2Fvim.git patch 9.1.1552: [security]: path traversal issue in tar.vim Problem: [security]: path traversal issue in tar.vim (@ax) Solution: warn the user for such things, drop leading /, don't forcefully overwrite files when writing temporary files, refactor autoload/tar.vim tar.vim: drop leading / in path names A tar archive containing files with leading `/` may cause confusions as to where the content is extracted. Let's make sure we drop the leading `/` and use a relative path instead. Also while at it, had to refactor it quite a bit and increase the minimum supported Vim version to v9. Also add a test for some basic tar functionality closes: #17733 --- diff --git a/Filelist b/Filelist index 3c9f783018..41eba31075 100644 --- a/Filelist +++ b/Filelist @@ -213,7 +213,9 @@ SRC_ALL = \ src/testdir/samples/*.txt \ src/testdir/samples/*.vim \ src/testdir/samples/evil.zip \ + src/testdir/samples/evil.tar \ src/testdir/samples/poc.zip \ + src/testdir/samples/sample.tar \ src/testdir/samples/test.zip \ src/testdir/samples/test000 \ src/testdir/samples/test_undo.txt.undo \ diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim index 7c1cefa63e..1a0d4f8a3d 100644 --- a/runtime/autoload/tar.vim +++ b/runtime/autoload/tar.vim @@ -16,6 +16,7 @@ " instead of shelling out to file(1) " 2025 Apr 16 by Vim Project: decouple from netrw by adding s:WinPath() " 2025 May 19 by Vim Project: restore working directory after read/write +" 2025 Jul 13 by Vim Project: warn with path traversal attacks " " Contains many ideas from Michael Toren's " @@ -34,9 +35,9 @@ if &cp || exists("g:loaded_tar") finish endif let g:loaded_tar= "v32b" -if v:version < 702 +if v:version < 900 echohl WarningMsg - echo "***warning*** this version of tar needs vim 7.2" + echo "***warning*** this version of tar needs vim 9.0" echohl Normal finish endif @@ -46,10 +47,10 @@ set cpo&vim " --------------------------------------------------------------------- " Default Settings: {{{1 if !exists("g:tar_browseoptions") - let g:tar_browseoptions= "Ptf" + let g:tar_browseoptions= "tf" endif if !exists("g:tar_readoptions") - let g:tar_readoptions= "pPxf" + let g:tar_readoptions= "pxf" endif if !exists("g:tar_cmd") let g:tar_cmd= "tar" @@ -58,6 +59,7 @@ if !exists("g:tar_writeoptions") let g:tar_writeoptions= "uf" endif if !exists("g:tar_delfile") + " Note: not supported on BSD let g:tar_delfile="--delete -f" endif if !exists("g:netrw_cygwin") @@ -106,10 +108,26 @@ if !exists("g:tar_shq") endif endif +let g:tar_secure=' -- ' +let g:tar_leading_pat='^\%([.]\{,2\}/\)\+' + " ---------------- " Functions: {{{1 " ---------------- +" --------------------------------------------------------------------- +" s:Msg: {{{2 +fun! s:Msg(func, severity, msg) + redraw! + if a:severity =~? 'error' + echohl Error + else + echohl WarningMsg + endif + echo $"***{a:severity}*** ({a:func}) {a:msg}" + echohl None +endfunc + " --------------------------------------------------------------------- " tar#Browse: {{{2 fun! tar#Browse(tarfile) @@ -118,16 +136,14 @@ fun! tar#Browse(tarfile) " sanity checks if !executable(g:tar_cmd) - redraw! - echohl Error | echo '***error*** (tar#Browse) "'.g:tar_cmd.'" not available on your system' + call s:Msg('tar#Browse', 'error', $"{g:tar_cmd} not available on your system") let &report= repkeep return endif if !filereadable(a:tarfile) if a:tarfile !~# '^\a\+://' " if it's an url, don't complain, let url-handlers such as vim do its thing - redraw! - echohl Error | echo "***error*** (tar#Browse) File not readable<".a:tarfile.">" | echohl None + call s:Msg('tar#Browse', 'error', $"File not readable<{a:tarfile}>") endif let &report= repkeep return @@ -203,28 +219,18 @@ fun! tar#Browse(tarfile) exe "sil! r! ".g:tar_cmd." -".g:tar_browseoptions." ".shellescape(tarfile,1) endif if v:shell_error != 0 - redraw! - echohl WarningMsg | echo "***warning*** (tar#Browse) please check your g:tar_browseoptions<".g:tar_browseoptions.">" + call s:Msg('tar#Browse', 'warning', $"please check your g:tar_browseoptions '<{g:tar_browseoptions}>'") return endif - " - " The following should not be neccessary, since in case of errors the - " previous if statement should have caught the problem (because tar exited - " with a non-zero exit code). - " if line("$") == curlast || ( line("$") == (curlast + 1) && - " \ getline("$") =~# '\c\<\%(warning\|error\|inappropriate\|unrecognized\)\>' && - " \ getline("$") =~ '\s' ) - " redraw! - " echohl WarningMsg | echo "***warning*** (tar#Browse) ".a:tarfile." doesn't appear to be a tar file" | echohl None - " keepj sil! %d - " let eikeep= &ei - " set ei=BufReadCmd,FileReadCmd - " exe "r ".fnameescape(a:tarfile) - " let &ei= eikeep - " keepj sil! 1d - " call Dret("tar#Browse : a:tarfile<".a:tarfile.">") - " return - " endif + + " remove tar: Removing leading '/' from member names + " Note: the message could be localized + if search('^tar: ') > 0 || search(g:tar_leading_pat) > 0 + call append(3,'" Note: Path Traversal Attack detected!') + let b:leading_slash = 1 + " remove the message output + sil g/^tar: /d + endif " set up maps supported for tar setlocal noma nomod ro @@ -243,12 +249,7 @@ fun! s:TarBrowseSelect() let repkeep= &report set report=10 let fname= getline(".") - - if !exists("g:tar_secure") && fname =~ '^\s*-\|\s\+-' - redraw! - echohl WarningMsg | echo '***warning*** (tar#BrowseSelect) rejecting tarfile member<'.fname.'> because of embedded "-"' - return - endif + let ls= get(b:, 'leading_slash', 0) " sanity check if fname =~ '^"' @@ -270,7 +271,8 @@ fun! s:TarBrowseSelect() wincmd _ endif let s:tblfile_{winnr()}= curfile - call tar#Read("tarfile:".tarfile.'::'.fname,1) + let b:leading_slash= ls + call tar#Read("tarfile:".tarfile.'::'.fname) filetype detect set nomod exe 'com! -buffer -nargs=? -complete=file TarDiff :call tar#Diff(,"'.fnameescape(fname).'")' @@ -280,26 +282,18 @@ endfun " --------------------------------------------------------------------- " tar#Read: {{{2 -fun! tar#Read(fname,mode) +fun! tar#Read(fname) let repkeep= &report set report=10 let tarfile = substitute(a:fname,'tarfile:\(.\{-}\)::.*$','\1','') let fname = substitute(a:fname,'tarfile:.\{-}::\(.*\)$','\1','') " be careful not to execute special crafted files - let escape_file = fname->fnameescape() - - " changing the directory to the temporary earlier to allow tar to extract the file with permissions intact - if !exists("*mkdir") - redraw! - echohl Error | echo "***error*** (tar#Write) sorry, mkdir() doesn't work on your system" | echohl None - let &report= repkeep - return - endif + let escape_file = fname->substitute(g:tar_leading_pat, '', '')->fnameescape() let curdir= getcwd() + let b:curdir= curdir let tmpdir= tempname() - let b:curdir= tmpdir - let b:tmpdir= curdir + let b:tmpdir= tmpdir if tmpdir =~ '\.' let tmpdir= substitute(tmpdir,'\.[^.]*$','','e') endif @@ -309,8 +303,7 @@ fun! tar#Read(fname,mode) try exe "lcd ".fnameescape(tmpdir) catch /^Vim\%((\a\+)\)\=:E344/ - redraw! - echohl Error | echo "***error*** (tar#Write) cannot lcd to temporary directory" | Echohl None + call s:Msg('tar#Read', 'error', "cannot lcd to temporary directory") let &report= repkeep return endtry @@ -333,7 +326,7 @@ fun! tar#Read(fname,mode) elseif fname =~ '\.bz3$' && executable("bz3cat") let decmp= "|bz3cat" let doro = 1 - elseif fname =~ '\.t\=gz$' && executable("zcat") + elseif fname =~ '\.t\=gz$' && executable("zcat") let decmp= "|zcat" let doro = 1 elseif fname =~ '\.lzma$' && executable("lzcat") @@ -356,68 +349,66 @@ fun! tar#Read(fname,mode) endif endif - if exists("g:tar_secure") - let tar_secure= " -- " - else - let tar_secure= " " - endif if tarfile =~# '\.bz2$' - exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif tarfile =~# '\.bz3$' - exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif tarfile =~# '\.\(gz\)$' - exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif tarfile =~# '\(\.tgz\|\.tbz\|\.txz\)' let filekind= s:Header(tarfile) if filekind =~? "bzip2" - exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! bzip2 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif filekind =~ "bzip3" - exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! bzip3 -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif filekind =~? "xz" - exe "sil! r! xz -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! xz -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif filekind =~? "zstd" - exe "sil! r! zstd --decompress --stdout -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! zstd --decompress --stdout -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif filekind =~? "gzip" - exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! gzip -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file endif elseif tarfile =~# '\.lrp$' - exe "sil! r! cat -- ".shellescape(tarfile,1)." | gzip -d -c - | ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! cat -- ".shellescape(tarfile,1)." | gzip -d -c - | ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif tarfile =~# '\.lzma$' - exe "sil! r! lzma -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! lzma -d -c -- ".shellescape(tarfile,1)."| ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif tarfile =~# '\.\(xz\|txz\)$' - exe "sil! r! xz --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! xz --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file elseif tarfile =~# '\.\(lz4\|tlz4\)$' - exe "sil! r! lz4 --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".tar_secure.shellescape(fname,1).decmp + exe "sil! r! lz4 --decompress --stdout -- ".shellescape(tarfile,1)." | ".g:tar_cmd." -".g:tar_readoptions." - ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file else if tarfile =~ '^\s*-' " A file name starting with a dash is taken as an option. Prepend ./ to avoid that. let tarfile = substitute(tarfile, '-', './-', '') endif - exe "silent r! ".g:tar_cmd." -".g:tar_readoptions.shellescape(tarfile,1)." ".tar_secure.shellescape(fname,1).decmp + exe "silent r! ".g:tar_cmd." -".g:tar_readoptions.shellescape(tarfile,1)." ".g:tar_secure.shellescape(fname,1).decmp exe "read ".escape_file endif + if get(b:, 'leading_slash', 0) + sil g/^tar: /d + endif redraw! -if v:shell_error != 0 + if v:shell_error != 0 lcd .. call s:Rmdir("_ZIPVIM_") exe "lcd ".fnameescape(curdir) - echohl Error | echo "***error*** (tar#Read) sorry, unable to open or extract ".tarfile." with ".fname | echohl None + call s:Msg('tar#Read', 'error', $"sorry, unable to open or extract {tarfile} with {fname}") endif if doro @@ -426,7 +417,6 @@ if v:shell_error != 0 endif let b:tarfile= a:fname - exe "file tarfile::".fnameescape(fname) " cleanup keepj sil! 0d @@ -434,7 +424,7 @@ if v:shell_error != 0 let &report= repkeep exe "lcd ".fnameescape(curdir) - silent exe "file tarfile::".escape_file + silent exe "file tarfile::". fname->fnameescape() endfun " --------------------------------------------------------------------- @@ -446,22 +436,35 @@ fun! tar#Write(fname) let curdir= b:curdir let tmpdir= b:tmpdir - if !exists("g:tar_secure") && a:fname =~ '^\s*-\|\s\+-' - redraw! - echohl WarningMsg | echo '***warning*** (tar#Write) rejecting tarfile member<'.a:fname.'> because of embedded "-"' - return - endif - " sanity checks if !executable(g:tar_cmd) redraw! let &report= repkeep return endif - let tarfile = substitute(b:tarfile,'tarfile:\(.\{-}\)::.*$','\1','') let fname = substitute(b:tarfile,'tarfile:.\{-}::\(.*\)$','\1','') + if get(b:, 'leading_slash', 0) + call s:Msg('tar#Write', 'error', $"sorry, not attempting to update {tarfile} with {fname}") + let &report= repkeep + return + endif + + if !isdirectory(fnameescape(tmpdir)) + call mkdir(fnameescape(tmpdir), 'p') + endif + exe $"lcd {fnameescape(tmpdir)}" + if isdirectory("_ZIPVIM_") + call s:Rmdir("_ZIPVIM_") + endif + call mkdir("_ZIPVIM_") + lcd _ZIPVIM_ + let dir = fnamemodify(fname, ':p:h') + if dir !~# '_ZIPVIM_$' + call mkdir(dir) + endif + " handle compressed archives if tarfile =~# '\.bz2' call system("bzip2 -d -- ".shellescape(tarfile,0)) @@ -500,8 +503,7 @@ fun! tar#Write(fname) " Note: no support for name.tar.tbz/.txz/.tgz/.tlz4/.tzst if v:shell_error != 0 - redraw! - echohl Error | echo "***error*** (tar#Write) sorry, unable to update ".tarfile." with ".fname | echohl None + call s:Msg('tar#Write', 'error', $"sorry, unable to update {tarfile} with {fname}") else if fname =~ '/' @@ -519,28 +521,22 @@ fun! tar#Write(fname) let tarfile = substitute(tarfile, '-', './-', '') endif - if exists("g:tar_secure") - let tar_secure= " -- " - else - let tar_secure= " " - endif - exe "w! ".fnameescape(fname) + " don't overwrite a file forcefully + exe "w ".fnameescape(fname) if has("win32unix") && executable("cygpath") let tarfile = substitute(system("cygpath ".shellescape(tarfile,0)),'\n','','e') endif " delete old file from tarfile - call system(g:tar_cmd." ".g:tar_delfile." ".shellescape(tarfile,0).tar_secure.shellescape(fname,0)) + " Note: BSD tar does not support --delete flag + call system(g:tar_cmd." ".g:tar_delfile." ".shellescape(tarfile,0).g:tar_secure.shellescape(fname,0)) if v:shell_error != 0 - redraw! - echohl Error | echo "***error*** (tar#Write) sorry, unable to update ".fnameescape(tarfile)." with ".fnameescape(fname) | echohl None + call s:Msg('tar#Write', 'error', $"sorry, unable to update {fnameescape(tarfile)} with {fnameescape(fname)} --delete not supported?") else - " update tarfile with new file - call system(g:tar_cmd." -".g:tar_writeoptions." ".shellescape(tarfile,0).tar_secure.shellescape(fname,0)) + call system(g:tar_cmd." -".g:tar_writeoptions." ".shellescape(tarfile,0).g:tar_secure.shellescape(fname,0)) if v:shell_error != 0 - redraw! - echohl Error | echo "***error*** (tar#Write) sorry, unable to update ".fnameescape(tarfile)." with ".fnameescape(fname) | echohl None + call s:Msg('tar#Write', 'error', $"sorry, unable to update {fnameescape(tarfile)} with {fnameescape(fname)}") elseif exists("compress") call system(compress) if exists("tgz") @@ -581,6 +577,7 @@ fun! tar#Diff(userfname,fname) if a:userfname != "" let fname= a:userfname endif + exe "lcd ".fnameescape(b:tmpdir). '/_ZIPVIM_' if filereadable(fname) " sets current file (from tarball) for diff'ing " splits window vertically @@ -604,12 +601,6 @@ fun! tar#Extract() set report=10 let fname= getline(".") - if !exists("g:tar_secure") && fname =~ '^\s*-\|\s\+-' - redraw! - echohl WarningMsg | echo '***warning*** (tar#BrowseSelect) rejecting tarfile member<'.fname.'> because of embedded "-"' - return - endif - " sanity check if fname =~ '^"' let &report= repkeep @@ -623,16 +614,16 @@ fun! tar#Extract() if filereadable(tarbase.".tar") call system(extractcmd." ".shellescape(tarbase).".tar ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar {fname}: failed!") else - echo "***note*** successfully extracted ".fname + echo "***note*** successfully extracted ". fname endif elseif filereadable(tarbase.".tgz") let extractcmd= substitute(extractcmd,"-","-z","") call system(extractcmd." ".shellescape(tarbase).".tgz ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tgz ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tgz {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -641,7 +632,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-z","") call system(extractcmd." ".shellescape(tarbase).".tar.gz ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.gz ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.gz {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -650,7 +641,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-j","") call system(extractcmd." ".shellescape(tarbase).".tbz ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd."j ".tarbase.".tbz ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tbz {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -659,7 +650,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-j","") call system(extractcmd." ".shellescape(tarbase).".tar.bz2 ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd."j ".tarbase.".tar.bz2 ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.bz2 {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -668,7 +659,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-j","") call system(extractcmd." ".shellescape(tarbase).".tar.bz3 ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd."j ".tarbase.".tar.bz3 ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.bz3 {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -677,7 +668,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-J","") call system(extractcmd." ".shellescape(tarbase).".txz ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".txz ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.txz {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -686,7 +677,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-J","") call system(extractcmd." ".shellescape(tarbase).".tar.xz ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.xz ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.xz {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -695,7 +686,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","--zstd","") call system(extractcmd." ".shellescape(tarbase).".tzst ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tzst ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tzst {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -704,7 +695,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","--zstd","") call system(extractcmd." ".shellescape(tarbase).".tar.zst ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.zst ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.zst {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -713,7 +704,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-I lz4","") call system(extractcmd." ".shellescape(tarbase).".tlz4 ".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tlz4 ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tlz4 {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -722,7 +713,7 @@ fun! tar#Extract() let extractcmd= substitute(extractcmd,"-","-I lz4","") call system(extractcmd." ".shellescape(tarbase).".tar.lz4".shellescape(fname)) if v:shell_error != 0 - echohl Error | echo "***error*** ".extractcmd." ".tarbase.".tar.lz4 ".fname.": failed!" | echohl NONE + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.lz4 {fname}: failed!") else echo "***note*** successfully extracted ".fname endif @@ -735,15 +726,7 @@ endfun " --------------------------------------------------------------------- " s:Rmdir: {{{2 fun! s:Rmdir(fname) - if has("unix") - call system("/bin/rm -rf -- ".shellescape(a:fname,0)) - elseif has("win32") || has("win95") || has("win64") || has("win16") - if &shell =~? "sh$" - call system("/bin/rm -rf -- ".shellescape(a:fname,0)) - else - call system("del /S ".shellescape(a:fname,0)) - endif - endif + call delete(a:fname, 'rf') endfun " s:FileHeader: {{{2 diff --git a/runtime/doc/pi_tar.txt b/runtime/doc/pi_tar.txt index 6d49928dcb..52706e93ef 100644 --- a/runtime/doc/pi_tar.txt +++ b/runtime/doc/pi_tar.txt @@ -1,11 +1,10 @@ -*pi_tar.txt* For Vim version 9.1. Last change: 2025 Mar 16 +*pi_tar.txt* For Vim version 9.1. Last change: 2025 Jul 15 +====================+ | Tar File Interface | +====================+ -Author: Charles E. Campbell - (remove NOSPAM from Campbell's email first) +Original Author: Charles E. Campbell Copyright 2005-2017: *tar-copyright* The VIM LICENSE (see |copyright|) applies to the files in this package, including tarPlugin.vim, tar.vim, and pi_tar.txt. Like @@ -61,7 +60,7 @@ Copyright 2005-2017: *tar-copyright* the file mentioned in the tarball. If the current directory is not correct for that path, :TarDiff will fail to find the associated file. - If the [filename] is given, that that filename (and path) will be used + If the [filename] is given, that filename (and path) will be used to specify the associated file. @@ -95,24 +94,25 @@ Copyright 2005-2017: *tar-copyright* *g:tar_readoptions* "OPxf" used to extract a file from a tarball *g:tar_cmd* "tar" the name of the tar program *g:tar_nomax* 0 if true, file window will not be maximized - *g:tar_secure* undef if exists: - "--"s will be used to prevent unwanted - option expansion in tar commands. - Please be sure that your tar command - accepts "--"; Posix compliant tar - utilities do accept them. - if not exists: - The tar plugin will reject any tar - files or member files that begin with - "-" - Not all tar's support the "--" which is why - it isn't default. *g:tar_writeoptions* "uf" used to update/replace a file ============================================================================== 4. History *tar-history* + unreleased: + Jul 13, 2025 * drop leading / + May 19, 2025 * restore working directory after read/write + Apr 16, 2025 * decouple from netrw by adding s:WinPath() + instead of shelling out to file(1) + Mar 02, 2025 * determine the compression using readblob() + Mar 02, 2025 * escape the filename before using :read + Mar 01, 2025 * fix syntax error in tar#Read() + Feb 28, 2025 * add support for bzip3 (#16755) + Feb 06, 2025 * add support for lz4 (#16591) + Nov 11, 2024 * support permissions (#7379) + Feb 19, 2024 * announce adoption + Jan 08, 2024 * fix a few problems (#138331, #12637, #8109) v31 Apr 02, 2017 * (klartext) reported that browsing encrypted files in a zip archive created unencrypted swap files. I am applying a similar fix diff --git a/runtime/doc/tags b/runtime/doc/tags index 384b27aa62..8251c73d41 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -7857,7 +7857,6 @@ g:tar_copycmd pi_tar.txt /*g:tar_copycmd* g:tar_extractcmd pi_tar.txt /*g:tar_extractcmd* g:tar_nomax pi_tar.txt /*g:tar_nomax* g:tar_readoptions pi_tar.txt /*g:tar_readoptions* -g:tar_secure pi_tar.txt /*g:tar_secure* g:tar_writeoptions pi_tar.txt /*g:tar_writeoptions* g:termdebug_config terminal.txt /*g:termdebug_config* g:termdebugger terminal.txt /*g:termdebugger* diff --git a/runtime/plugin/tarPlugin.vim b/runtime/plugin/tarPlugin.vim index 825b7ae17f..e55a367854 100644 --- a/runtime/plugin/tarPlugin.vim +++ b/runtime/plugin/tarPlugin.vim @@ -23,14 +23,14 @@ set cpo&vim " Public Interface: {{{1 augroup tar au! - au BufReadCmd tarfile::* call tar#Read(expand(""), 1) - au FileReadCmd tarfile::* call tar#Read(expand(""), 0) + au BufReadCmd tarfile::* call tar#Read(expand("")) + au FileReadCmd tarfile::* call tar#Read(expand("")) au BufWriteCmd tarfile::* call tar#Write(expand("")) au FileWriteCmd tarfile::* call tar#Write(expand("")) if has("unix") - au BufReadCmd tarfile::*/* call tar#Read(expand(""), 1) - au FileReadCmd tarfile::*/* call tar#Read(expand(""), 0) + au BufReadCmd tarfile::*/* call tar#Read(expand("")) + au FileReadCmd tarfile::*/* call tar#Read(expand("")) au BufWriteCmd tarfile::*/* call tar#Write(expand("")) au FileWriteCmd tarfile::*/* call tar#Write(expand("")) endif diff --git a/src/po/vim.pot b/src/po/vim.pot index 7d28c12df7..4a9a20bcde 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-07-15 21:42+0200\n" +"POT-Creation-Date: 2025-07-15 21:50+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -4257,327 +4257,327 @@ msgstr "" msgid "%s (%s, compiled %s)" msgstr "" -#: ../version.c:4036 +#: ../version.c:4038 msgid "" "\n" "MS-Windows ARM64 GUI/console version" msgstr "" -#: ../version.c:4038 +#: ../version.c:4040 msgid "" "\n" "MS-Windows 64-bit GUI/console version" msgstr "" -#: ../version.c:4041 +#: ../version.c:4043 msgid "" "\n" "MS-Windows 32-bit GUI/console version" msgstr "" -#: ../version.c:4046 +#: ../version.c:4048 msgid "" "\n" "MS-Windows ARM64 GUI version" msgstr "" -#: ../version.c:4048 +#: ../version.c:4050 msgid "" "\n" "MS-Windows 64-bit GUI version" msgstr "" -#: ../version.c:4051 +#: ../version.c:4053 msgid "" "\n" "MS-Windows 32-bit GUI version" msgstr "" -#: ../version.c:4055 +#: ../version.c:4057 msgid " with OLE support" msgstr "" -#: ../version.c:4060 +#: ../version.c:4062 msgid "" "\n" "MS-Windows ARM64 console version" msgstr "" -#: ../version.c:4062 +#: ../version.c:4064 msgid "" "\n" "MS-Windows 64-bit console version" msgstr "" -#: ../version.c:4065 +#: ../version.c:4067 msgid "" "\n" "MS-Windows 32-bit console version" msgstr "" -#: ../version.c:4071 +#: ../version.c:4073 msgid "" "\n" "macOS version" msgstr "" -#: ../version.c:4073 +#: ../version.c:4075 msgid "" "\n" "macOS version w/o darwin feat." msgstr "" -#: ../version.c:4083 +#: ../version.c:4085 msgid "" "\n" "OpenVMS version" msgstr "" -#: ../version.c:4098 +#: ../version.c:4100 msgid "" "\n" "Included patches: " msgstr "" -#: ../version.c:4123 +#: ../version.c:4125 msgid "" "\n" "Extra patches: " msgstr "" -#: ../version.c:4135 ../version.c:4446 +#: ../version.c:4137 ../version.c:4448 msgid "Modified by " msgstr "" -#: ../version.c:4142 +#: ../version.c:4144 msgid "" "\n" "Compiled " msgstr "" -#: ../version.c:4145 +#: ../version.c:4147 msgid "by " msgstr "" -#: ../version.c:4157 +#: ../version.c:4159 msgid "" "\n" "Huge version " msgstr "" -#: ../version.c:4159 +#: ../version.c:4161 msgid "" "\n" "Normal version " msgstr "" -#: ../version.c:4161 +#: ../version.c:4163 msgid "" "\n" "Tiny version " msgstr "" -#: ../version.c:4164 +#: ../version.c:4166 msgid "without GUI." msgstr "" -#: ../version.c:4167 +#: ../version.c:4169 msgid "with GTK3 GUI." msgstr "" -#: ../version.c:4169 +#: ../version.c:4171 msgid "with GTK2-GNOME GUI." msgstr "" -#: ../version.c:4171 +#: ../version.c:4173 msgid "with GTK2 GUI." msgstr "" -#: ../version.c:4174 +#: ../version.c:4176 msgid "with X11-Motif GUI." msgstr "" -#: ../version.c:4176 +#: ../version.c:4178 msgid "with Haiku GUI." msgstr "" -#: ../version.c:4178 +#: ../version.c:4180 msgid "with Photon GUI." msgstr "" -#: ../version.c:4180 +#: ../version.c:4182 msgid "with GUI." msgstr "" -#: ../version.c:4182 +#: ../version.c:4184 msgid " Features included (+) or not (-):\n" msgstr "" -#: ../version.c:4189 +#: ../version.c:4191 msgid " system vimrc file: \"" msgstr "" -#: ../version.c:4194 +#: ../version.c:4196 msgid " user vimrc file: \"" msgstr "" -#: ../version.c:4199 +#: ../version.c:4201 msgid " 2nd user vimrc file: \"" msgstr "" -#: ../version.c:4204 ../version.c:4211 ../version.c:4215 +#: ../version.c:4206 ../version.c:4213 ../version.c:4217 msgid " 3rd user vimrc file: \"" msgstr "" -#: ../version.c:4207 +#: ../version.c:4209 msgid " 4th user vimrc file: \"" msgstr "" -#: ../version.c:4220 +#: ../version.c:4222 msgid " user exrc file: \"" msgstr "" -#: ../version.c:4225 +#: ../version.c:4227 msgid " 2nd user exrc file: \"" msgstr "" -#: ../version.c:4231 +#: ../version.c:4233 msgid " system gvimrc file: \"" msgstr "" -#: ../version.c:4235 +#: ../version.c:4237 msgid " user gvimrc file: \"" msgstr "" -#: ../version.c:4239 +#: ../version.c:4241 msgid "2nd user gvimrc file: \"" msgstr "" -#: ../version.c:4244 +#: ../version.c:4246 msgid "3rd user gvimrc file: \"" msgstr "" -#: ../version.c:4249 +#: ../version.c:4251 msgid " defaults file: \"" msgstr "" -#: ../version.c:4254 +#: ../version.c:4256 msgid " system menu file: \"" msgstr "" -#: ../version.c:4262 +#: ../version.c:4264 msgid " fall-back for $VIM: \"" msgstr "" -#: ../version.c:4268 +#: ../version.c:4270 msgid " f-b for $VIMRUNTIME: \"" msgstr "" -#: ../version.c:4272 +#: ../version.c:4274 msgid "Compilation: " msgstr "" -#: ../version.c:4278 +#: ../version.c:4280 msgid "Compiler: " msgstr "" -#: ../version.c:4283 +#: ../version.c:4285 msgid "Linking: " msgstr "" -#: ../version.c:4288 +#: ../version.c:4290 msgid " DEBUG BUILD" msgstr "" -#: ../version.c:4324 +#: ../version.c:4326 msgid "VIM - Vi IMproved" msgstr "" -#: ../version.c:4326 +#: ../version.c:4328 msgid "version " msgstr "" -#: ../version.c:4327 +#: ../version.c:4329 msgid "by Bram Moolenaar et al." msgstr "" -#: ../version.c:4331 +#: ../version.c:4333 msgid "Vim is open source and freely distributable" msgstr "" -#: ../version.c:4333 +#: ../version.c:4335 msgid "Help poor children in Uganda!" msgstr "" -#: ../version.c:4334 +#: ../version.c:4336 msgid "type :help iccf for information " msgstr "" -#: ../version.c:4336 +#: ../version.c:4338 msgid "type :q to exit " msgstr "" -#: ../version.c:4337 +#: ../version.c:4339 msgid "type :help or for on-line help" msgstr "" -#: ../version.c:4338 +#: ../version.c:4340 msgid "type :help version9 for version info" msgstr "" -#: ../version.c:4341 +#: ../version.c:4343 msgid "Running in Vi compatible mode" msgstr "" -#: ../version.c:4342 +#: ../version.c:4344 msgid "type :set nocp for Vim defaults" msgstr "" -#: ../version.c:4343 +#: ../version.c:4345 msgid "type :help cp-default for info on this" msgstr "" -#: ../version.c:4358 +#: ../version.c:4360 msgid "menu Help->Orphans for information " msgstr "" -#: ../version.c:4360 +#: ../version.c:4362 msgid "Running modeless, typed text is inserted" msgstr "" -#: ../version.c:4361 +#: ../version.c:4363 msgid "menu Edit->Global Settings->Toggle Insert Mode " msgstr "" -#: ../version.c:4362 +#: ../version.c:4364 msgid " for two modes " msgstr "" -#: ../version.c:4366 +#: ../version.c:4368 msgid "menu Edit->Global Settings->Toggle Vi Compatible" msgstr "" -#: ../version.c:4367 +#: ../version.c:4369 msgid " for Vim defaults " msgstr "" -#: ../version.c:4408 +#: ../version.c:4410 msgid "Sponsor Vim development!" msgstr "" -#: ../version.c:4409 +#: ../version.c:4411 msgid "Become a registered Vim user!" msgstr "" -#: ../version.c:4412 +#: ../version.c:4414 msgid "type :help sponsor for information " msgstr "" -#: ../version.c:4413 +#: ../version.c:4415 msgid "type :help register for information " msgstr "" -#: ../version.c:4415 +#: ../version.c:4417 msgid "menu Help->Sponsor/Register for information " msgstr "" diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak index c0e3e60d31..e61bf2b50e 100644 --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -245,6 +245,7 @@ NEW_TESTS = \ test_plugin_helptoc \ test_plugin_man \ test_plugin_matchparen \ + test_plugin_tar \ test_plugin_termdebug \ test_plugin_tohtml \ test_plugin_tutor \ @@ -517,6 +518,7 @@ NEW_TESTS_RES = \ test_plugin_helptoc.res \ test_plugin_man.res \ test_plugin_matchparen.res \ + test_plugin_tar.res \ test_plugin_termdebug.res \ test_plugin_tohtml.res \ test_plugin_tutor.res \ diff --git a/src/testdir/samples/evil.tar b/src/testdir/samples/evil.tar new file mode 100644 index 0000000000..8cbc061fdf Binary files /dev/null and b/src/testdir/samples/evil.tar differ diff --git a/src/testdir/samples/sample.tar b/src/testdir/samples/sample.tar new file mode 100644 index 0000000000..4da3bf35e6 Binary files /dev/null and b/src/testdir/samples/sample.tar differ diff --git a/src/testdir/test_plugin_tar.vim b/src/testdir/test_plugin_tar.vim new file mode 100644 index 0000000000..ebf74d7daa --- /dev/null +++ b/src/testdir/test_plugin_tar.vim @@ -0,0 +1,128 @@ +vim9script + +CheckExecutable tar +CheckNotMSWindows + +runtime plugin/tarPlugin.vim + +def CopyFile(source: string) + if !filecopy($"samples/{source}", "X.tar") + assert_report($"Can't copy samples/{source}") + endif +enddef + +def g:Test_tar_basic() + CopyFile("sample.tar") + defer delete("X.tar") + defer delete("./testtar", 'rf') + e X.tar + + ### Check header + assert_match('^" tar\.vim version v\d\+', getline(1)) + assert_match('^" Browsing tarfile .*/X.tar', getline(2)) + assert_match('^" Select a file with cursor and press ENTER, "x" to extract a file', getline(3)) + assert_match('^$', getline(4)) + assert_match('testtar/', getline(5)) + assert_match('testtar/file1.txt', getline(6)) + + ### Check ENTER on header + :1 + exe ":normal \" + assert_equal("X.tar", @%) + + ### Check ENTER on file + :6 + exe ":normal \" + assert_equal("tarfile::testtar/file1.txt", @%) + + + ### Check editing file + ### Note: deleting entries not supported on BSD + if has("mac") + return + endif + if has("bsd") + return + endif + s/.*/some-content/ + assert_equal("some-content", getline(1)) + w! + assert_equal("tarfile::testtar/file1.txt", @%) + bw! + close + bw! + + e X.tar + :6 + exe "normal \" + assert_equal("some-content", getline(1)) + bw! + close + + ### Check extracting file + :5 + normal x + assert_true(filereadable("./testtar/file1.txt")) + bw! +enddef + +def g:Test_tar_evil() + CopyFile("evil.tar") + defer delete("X.tar") + defer delete("./etc", 'rf') + e X.tar + + ### Check header + assert_match('^" tar\.vim version v\d\+', getline(1)) + assert_match('^" Browsing tarfile .*/X.tar', getline(2)) + assert_match('^" Select a file with cursor and press ENTER, "x" to extract a file', getline(3)) + assert_match('^" Note: Path Traversal Attack detected', getline(4)) + assert_match('^$', getline(5)) + assert_match('/etc/ax-pwn', getline(6)) + + ### Check ENTER on header + :1 + exe ":normal \" + assert_equal("X.tar", @%) + assert_equal(1, b:leading_slash) + + ### Check ENTER on file + :6 + exe ":normal \" + assert_equal(1, b:leading_slash) + assert_equal("tarfile::/etc/ax-pwn", @%) + + + ### Check editing file + ### Note: deleting entries not supported on BSD + if has("mac") + return + endif + if has("bsd") + return + endif + s/.*/none/ + assert_equal("none", getline(1)) + w! + assert_equal(1, b:leading_slash) + assert_equal("tarfile::/etc/ax-pwn", @%) + bw! + close + bw! + + # Writing was aborted + e X.tar + assert_match('^" Note: Path Traversal Attack detected', getline(4)) + :6 + exe "normal \" + assert_equal("something", getline(1)) + bw! + close + + ### Check extracting file + :5 + normal x + assert_true(filereadable("./etc/ax-pwn")) + + bw! +enddef diff --git a/src/version.c b/src/version.c index 40c383a4b1..98c397cb61 100644 --- a/src/version.c +++ b/src/version.c @@ -719,6 +719,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1552, /**/ 1551, /**/