diff --git a/runtime/autoload/zip.vim b/runtime/autoload/zip.vim index 305b823e7b..3143f3f3ca 100644 --- a/runtime/autoload/zip.vim +++ b/runtime/autoload/zip.vim @@ -15,6 +15,7 @@ " 2024 Aug 18 by Vim Project: correctly handle special globbing chars " 2024 Aug 21 by Vim Project: simplify condition to detect MS-Windows " 2025 Mar 11 by Vim Project: handle filenames with leading '-' correctly +" 2025 Jul 12 by Vim Project: drop ../ on write to prevent path traversal attacks " License: Vim License (see vim's :help license) " Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1 " Permission is hereby granted to use and distribute this code, @@ -71,8 +72,9 @@ fun! s:Mess(group, msg) echohl Normal endfun -if v:version < 702 - call s:Mess('WarningMsg', "***warning*** this version of zip needs vim 7.2 or later") +if !has('nvim-0.10') && v:version < 901 + " required for defer + call s:Mess('WarningMsg', "***warning*** this version of zip needs vim 9.1 or later") finish endif " sanity checks @@ -235,59 +237,62 @@ endfun " zip#Write: {{{2 fun! zip#Write(fname) let dict = s:SetSaneOpts() + let need_rename = 0 defer s:RestoreOpts(dict) " sanity checks if !executable(substitute(g:zip_zipcmd,'\s\+.*$','','')) - call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't appear to have the ".g:zip_zipcmd." program") - return - endif - if !exists("*mkdir") - call s:Mess('Error', "***error*** (zip#Write) sorry, mkdir() doesn't work on your system") - return + call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't appear to have the ".g:zip_zipcmd." program") + return endif let curdir= getcwd() let tmpdir= tempname() if tmpdir =~ '\.' - let tmpdir= substitute(tmpdir,'\.[^.]*$','','e') + let tmpdir= substitute(tmpdir,'\.[^.]*$','','e') endif call mkdir(tmpdir,"p") " attempt to change to the indicated directory if s:ChgDir(tmpdir,s:ERROR,"(zip#Write) cannot cd to temporary directory") - return + return endif " place temporary files under .../_ZIPVIM_/ if isdirectory("_ZIPVIM_") - call delete("_ZIPVIM_", "rf") + call delete("_ZIPVIM_", "rf") endif call mkdir("_ZIPVIM_") cd _ZIPVIM_ if has("unix") - let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','') - let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','') + let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','') + let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','') else - let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','') - let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','') + let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','') + let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','') + endif + if fname =~ '^[.]\{1,2}/' + call system(g:zip_zipcmd." -d ".s:Escape(fnamemodify(zipfile,":p"),0)." ".s:Escape(fname,0)) + let fname = fname->substitute('^\([.]\{1,2}/\)\+', '', 'g') + let need_rename = 1 endif if fname =~ '/' - let dirpath = substitute(fname,'/[^/]\+$','','e') - if has("win32unix") && executable("cygpath") + let dirpath = substitute(fname,'/[^/]\+$','','e') + if has("win32unix") && executable("cygpath") let dirpath = substitute(system("cygpath ".s:Escape(dirpath,0)),'\n','','e') - endif - call mkdir(dirpath,"p") + endif + call mkdir(dirpath,"p") endif if zipfile !~ '/' - let zipfile= curdir.'/'.zipfile + let zipfile= curdir.'/'.zipfile endif - exe "w! ".fnameescape(fname) + " don't overwrite files forcefully + exe "w ".fnameescape(fname) if has("win32unix") && executable("cygpath") - let zipfile = substitute(system("cygpath ".s:Escape(zipfile,0)),'\n','','e') + let zipfile = substitute(system("cygpath ".s:Escape(zipfile,0)),'\n','','e') endif if (has("win32") || has("win95") || has("win64") || has("win16")) && &shell !~? 'sh$' @@ -296,21 +301,24 @@ fun! zip#Write(fname) call system(g:zip_zipcmd." -u ".s:Escape(fnamemodify(zipfile,":p"),0)." ".s:Escape(fname,0)) if v:shell_error != 0 - call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update ".zipfile." with ".fname) + call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update ".zipfile." with ".fname) elseif s:zipfile_{winnr()} =~ '^\a\+://' - " support writing zipfiles across a network - let netzipfile= s:zipfile_{winnr()} - 1split|enew - let binkeep= &binary - let eikeep = &ei - set binary ei=all - exe "noswapfile e! ".fnameescape(zipfile) - call netrw#NetWrite(netzipfile) - let &ei = eikeep - let &binary = binkeep - q! - unlet s:zipfile_{winnr()} + " support writing zipfiles across a network + let netzipfile= s:zipfile_{winnr()} + 1split|enew + let binkeep= &binary + let eikeep = &ei + set binary ei=all + exe "noswapfile e! ".fnameescape(zipfile) + call netrw#NetWrite(netzipfile) + let &ei = eikeep + let &binary = binkeep + q! + unlet s:zipfile_{winnr()} + elseif need_rename + exe $"sil keepalt file {fnameescape($"zipfile://{zipfile}::{fname}")}" + call s:Mess('Warning', "***error*** (zip#Browse) Path Traversal Attack detected, dropping relative path") endif " cleanup and restore current directory @@ -319,7 +327,6 @@ fun! zip#Write(fname) call s:ChgDir(curdir,s:WARNING,"(zip#Write) unable to return to ".curdir."!") call delete(tmpdir, "rf") setlocal nomod - endfun " --------------------------------------------------------------------- @@ -332,15 +339,18 @@ fun! zip#Extract() " sanity check if fname =~ '^"' - return + return endif if fname =~ '/$' - call s:Mess('Error', "***error*** (zip#Extract) Please specify a file, not a directory") - return + call s:Mess('Error', "***error*** (zip#Extract) Please specify a file, not a directory") + return + elseif fname =~ '^[.]\?[.]/' + call s:Mess('Error', "***error*** (zip#Browse) Path Traversal Attack detected, not extracting!") + return endif if filereadable(fname) - call s:Mess('Error', "***error*** (zip#Extract) <" .. fname .."> already exists in directory, not overwriting!") - return + call s:Mess('Error', "***error*** (zip#Extract) <" .. fname .."> already exists in directory, not overwriting!") + return endif let target = fname->substitute('\[', '[[]', 'g') " unzip 6.0 does not support -- to denote end-of-arguments @@ -362,13 +372,12 @@ fun! zip#Extract() " extract the file mentioned under the cursor call system($"{g:zip_extractcmd} -o {shellescape(b:zipfile)} {target}") if v:shell_error != 0 - call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!") + call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!") elseif !filereadable(fname) - call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!") + call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!") else - echomsg "***note*** successfully extracted ".fname + echomsg "***note*** successfully extracted ".fname endif - endfun " --------------------------------------------------------------------- diff --git a/runtime/doc/pi_zip.txt b/runtime/doc/pi_zip.txt index 95c2254cc3..cd9e776f80 100644 --- a/runtime/doc/pi_zip.txt +++ b/runtime/doc/pi_zip.txt @@ -111,6 +111,18 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell *zip-copyright* ============================================================================== 4. History *zip-history* {{{1 + unreleased: + Jul 12, 2025 * drop ../ on write to prevent path traversal attacks + Mar 11, 2025 * handle filenames with leading '-' correctly + Aug 21, 2024 * simplify condition to detect MS-Windows + Aug 18, 2024 * correctly handle special globbing chars + Aug 05, 2024 * clean-up and make it work with shellslash on Windows + Aug 05, 2024 * workaround for the FreeBSD's unzip + Aug 04, 2024 * escape '[' in name of file to be extracted + Jul 30, 2024 * fix opening remote zipfile + Jul 24, 2024 * use delete() function + Jul 23, 2024 * fix 'x' command + Jun 16, 2024 * handle whitespace on Windows properly (#14998) v33 Dec 07, 2021 * `*.xlam` mentioned twice in zipPlugin v32 Oct 22, 2021 * to avoid an issue with a vim 8.2 patch, zipfile: has been changed to zipfile:// . This often shows up diff --git a/test/old/testdir/samples/evil.zip b/test/old/testdir/samples/evil.zip new file mode 100644 index 0000000000..e0a7f96141 Binary files /dev/null and b/test/old/testdir/samples/evil.zip differ diff --git a/test/old/testdir/test_plugin_zip.vim b/test/old/testdir/test_plugin_zip.vim index ba0a6778bc..ac1e102a87 100644 --- a/test/old/testdir/test_plugin_zip.vim +++ b/test/old/testdir/test_plugin_zip.vim @@ -9,13 +9,14 @@ endif runtime plugin/zipPlugin.vim -func Test_zip_basic() - - "## get our zip file - if !filecopy("samples/test.zip", "X.zip") - call assert_report("Can't copy samples/test.zip") - return +func s:CopyZipFile(source) + if !filecopy($"samples/{a:source}", "X.zip") + call assert_report($"Can't copy samples/{a:source}.zip") endif +endfunc + +func Test_zip_basic() + call s:CopyZipFile("test.zip") defer delete("X.zip") e X.zip @@ -142,11 +143,7 @@ func Test_zip_glob_fname() CheckNotMSWindows " does not work on Windows, why? - "## copy sample zip file - if !filecopy("samples/testa.zip", "X.zip") - call assert_report("Can't copy samples/testa.zip") - return - endif + call s:CopyZipFile("testa.zip") defer delete("X.zip") defer delete('zipglob', 'rf') @@ -240,10 +237,7 @@ func Test_zip_fname_leading_hyphen() CheckNotMSWindows "## copy sample zip file - if !filecopy("samples/poc.zip", "X.zip") - call assert_report("Can't copy samples/poc.zip") - return - endif + call s:CopyZipFile("poc.zip") defer delete("X.zip") defer delete('-d', 'rf') defer delete('/tmp/pwned', 'rf') @@ -258,3 +252,26 @@ func Test_zip_fname_leading_hyphen() call assert_false(filereadable('/tmp/pwned')) bw endfunc + +func Test_zip_fname_evil_path() + CheckNotMSWindows + " needed for writing the zip file + CheckExecutable zip + + call s:CopyZipFile("evil.zip") + defer delete("X.zip") + e X.zip + + :1 + let fname = 'pwn' + call search('\V' .. fname) + normal x + call assert_false(filereadable('/etc/ax-pwn')) + let mess = execute(':mess') + call assert_match('Path Traversal Attack', mess) + + exe ":normal \" + :w + call assert_match('zipfile://.*::etc/ax-pwn', @%) + bw +endfunc