patch 9.1.1551: [security]: path traversal issue in zip.vim Commit: https://github.com/vim/vim/commit/586294a04179d855c3d1d4ee5ea83931963680b8 Author: Christian Brabandt <c...@256bit.org> Date: Tue Jul 15 21:43:01 2025 +0200
patch 9.1.1551: [security]: path traversal issue in zip.vim Problem: [security]: path traversal issue in zip.vim (@ax) Solution: drop leading ../ on write of zipfiles, don't forcefully overwrite existing files A zip plugin which contains filenames with leading '../' may cause confusion as to where the content will be extracted. Let's drop such things and make sure we use a relative filename instead and don't forcefully overwrite temporary files. Also, warn the user of such things. related: #17733 Signed-off-by: Christian Brabandt <c...@256bit.org> diff --git a/Filelist b/Filelist index 1d5a1e6de..3c9f78301 100644 --- a/Filelist +++ b/Filelist @@ -212,6 +212,7 @@ SRC_ALL = \ src/testdir/samples/*.html \ src/testdir/samples/*.txt \ src/testdir/samples/*.vim \ + src/testdir/samples/evil.zip \ src/testdir/samples/poc.zip \ src/testdir/samples/test.zip \ src/testdir/samples/test000 \ diff --git a/runtime/autoload/zip.vim b/runtime/autoload/zip.vim index dae4ddeb9..c46ec4470 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, @@ -236,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://\(.\{-}\)::[^\].*$',' ','') - let fname = substitute(a:fname,'zipfile://.\{-}::\([^\].*\)$',' ','') + let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\].*$',' ','') + let fname = substitute(a:fname,'zipfile://.\{-}::\([^\].*\)$',' ','') else - let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\].*$',' ','') - let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\].*\)$',' ','') + let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\].*$',' ','') + let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\].*\)$',' ','') + 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)),' ','','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)),' ','','e') + let zipfile = substitute(system("cygpath ".s:Escape(zipfile,0)),' ','','e') endif if (has("win32") || has("win95") || has("win64") || has("win16")) && &shell !~? 'sh$' @@ -297,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()} =~ '^ \+://' - " 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 @@ -320,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 " --------------------------------------------------------------------- @@ -333,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 @@ -363,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 afc2d0eeb..0f7ef4ec3 100644 --- a/runtime/doc/pi_zip.txt +++ b/runtime/doc/pi_zip.txt @@ -1,4 +1,4 @@ -*pi_zip.txt* For Vim version 9.1. Last change: 2025 Apr 02 +*pi_zip.txt* For Vim version 9.1. Last change: 2025 Jul 15 +====================+ | Zip File Interface | @@ -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/src/po/vim.pot b/src/po/vim.pot index 6bbe36591..7d28c12df 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION " "Report-Msgid-Bugs-To: " -"POT-Creation-Date: 2025-07-15 21:26+0200 " +"POT-Creation-Date: 2025-07-15 21:42+0200 " "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE " "Last-Translator: FULL NAME <EMAIL@ADDRESS> " "Language-Team: LANGUAGE <l...@li.org> " @@ -4257,327 +4257,327 @@ msgstr "" msgid "%s (%s, compiled %s)" msgstr "" -#: ../version.c:4034 +#: ../version.c:4036 msgid "" " " "MS-Windows ARM64 GUI/console version" msgstr "" -#: ../version.c:4036 +#: ../version.c:4038 msgid "" " " "MS-Windows 64-bit GUI/console version" msgstr "" -#: ../version.c:4039 +#: ../version.c:4041 msgid "" " " "MS-Windows 32-bit GUI/console version" msgstr "" -#: ../version.c:4044 +#: ../version.c:4046 msgid "" " " "MS-Windows ARM64 GUI version" msgstr "" -#: ../version.c:4046 +#: ../version.c:4048 msgid "" " " "MS-Windows 64-bit GUI version" msgstr "" -#: ../version.c:4049 +#: ../version.c:4051 msgid "" " " "MS-Windows 32-bit GUI version" msgstr "" -#: ../version.c:4053 +#: ../version.c:4055 msgid " with OLE support" msgstr "" -#: ../version.c:4058 +#: ../version.c:4060 msgid "" " " "MS-Windows ARM64 console version" msgstr "" -#: ../version.c:4060 +#: ../version.c:4062 msgid "" " " "MS-Windows 64-bit console version" msgstr "" -#: ../version.c:4063 +#: ../version.c:4065 msgid "" " " "MS-Windows 32-bit console version" msgstr "" -#: ../version.c:4069 +#: ../version.c:4071 msgid "" " " "macOS version" msgstr "" -#: ../version.c:4071 +#: ../version.c:4073 msgid "" " " "macOS version w/o darwin feat." msgstr "" -#: ../version.c:4081 +#: ../version.c:4083 msgid "" " " "OpenVMS version" msgstr "" -#: ../version.c:4096 +#: ../version.c:4098 msgid "" " " "Included patches: " msgstr "" -#: ../version.c:4121 +#: ../version.c:4123 msgid "" " " "Extra patches: " msgstr "" -#: ../version.c:4133 ../version.c:4444 +#: ../version.c:4135 ../version.c:4446 msgid "Modified by " msgstr "" -#: ../version.c:4140 +#: ../version.c:4142 msgid "" " " "Compiled " msgstr "" -#: ../version.c:4143 +#: ../version.c:4145 msgid "by " msgstr "" -#: ../version.c:4155 +#: ../version.c:4157 msgid "" " " "Huge version " msgstr "" -#: ../version.c:4157 +#: ../version.c:4159 msgid "" " " "Normal version " msgstr "" -#: ../version.c:4159 +#: ../version.c:4161 msgid "" " " "Tiny version " msgstr "" -#: ../version.c:4162 +#: ../version.c:4164 msgid "without GUI." msgstr "" -#: ../version.c:4165 +#: ../version.c:4167 msgid "with GTK3 GUI." msgstr "" -#: ../version.c:4167 +#: ../version.c:4169 msgid "with GTK2-GNOME GUI." msgstr "" -#: ../version.c:4169 +#: ../version.c:4171 msgid "with GTK2 GUI." msgstr "" -#: ../version.c:4172 +#: ../version.c:4174 msgid "with X11-Motif GUI." msgstr "" -#: ../version.c:4174 +#: ../version.c:4176 msgid "with Haiku GUI." msgstr "" -#: ../version.c:4176 +#: ../version.c:4178 msgid "with Photon GUI." msgstr "" -#: ../version.c:4178 +#: ../version.c:4180 msgid "with GUI." msgstr "" -#: ../version.c:4180 +#: ../version.c:4182 msgid " Features included (+) or not (-): " msgstr "" -#: ../version.c:4187 +#: ../version.c:4189 msgid " system vimrc file: \"" msgstr "" -#: ../version.c:4192 +#: ../version.c:4194 msgid " user vimrc file: \"" msgstr "" -#: ../version.c:4197 +#: ../version.c:4199 msgid " 2nd user vimrc file: \"" msgstr "" -#: ../version.c:4202 ../version.c:4209 ../version.c:4213 +#: ../version.c:4204 ../version.c:4211 ../version.c:4215 msgid " 3rd user vimrc file: \"" msgstr "" -#: ../version.c:4205 +#: ../version.c:4207 msgid " 4th user vimrc file: \"" msgstr "" -#: ../version.c:4218 +#: ../version.c:4220 msgid " user exrc file: \"" msgstr "" -#: ../version.c:4223 +#: ../version.c:4225 msgid " 2nd user exrc file: \"" msgstr "" -#: ../version.c:4229 +#: ../version.c:4231 msgid " system gvimrc file: \"" msgstr "" -#: ../version.c:4233 +#: ../version.c:4235 msgid " user gvimrc file: \"" msgstr "" -#: ../version.c:4237 +#: ../version.c:4239 msgid "2nd user gvimrc file: \"" msgstr "" -#: ../version.c:4242 +#: ../version.c:4244 msgid "3rd user gvimrc file: \"" msgstr "" -#: ../version.c:4247 +#: ../version.c:4249 msgid " defaults file: \"" msgstr "" -#: ../version.c:4252 +#: ../version.c:4254 msgid " system menu file: \"" msgstr "" -#: ../version.c:4260 +#: ../version.c:4262 msgid " fall-back for $VIM: \"" msgstr "" -#: ../version.c:4266 +#: ../version.c:4268 msgid " f-b for $VIMRUNTIME: \"" msgstr "" -#: ../version.c:4270 +#: ../version.c:4272 msgid "Compilation: " msgstr "" -#: ../version.c:4276 +#: ../version.c:4278 msgid "Compiler: " msgstr "" -#: ../version.c:4281 +#: ../version.c:4283 msgid "Linking: " msgstr "" -#: ../version.c:4286 +#: ../version.c:4288 msgid " DEBUG BUILD" msgstr "" -#: ../version.c:4322 +#: ../version.c:4324 msgid "VIM - Vi IMproved" msgstr "" -#: ../version.c:4324 +#: ../version.c:4326 msgid "version " msgstr "" -#: ../version.c:4325 +#: ../version.c:4327 msgid "by Bram Moolenaar et al." msgstr "" -#: ../version.c:4329 +#: ../version.c:4331 msgid "Vim is open source and freely distributable" msgstr "" -#: ../version.c:4331 +#: ../version.c:4333 msgid "Help poor children in Uganda!" msgstr "" -#: ../version.c:4332 +#: ../version.c:4334 msgid "type :help iccf<Enter> for information " msgstr "" -#: ../version.c:4334 +#: ../version.c:4336 msgid "type :q<Enter> to exit " msgstr "" -#: ../version.c:4335 +#: ../version.c:4337 msgid "type :help<Enter> or <F1> for on-line help" msgstr "" -#: ../version.c:4336 +#: ../version.c:4338 msgid "type :help version9<Enter> for version info" msgstr "" -#: ../version.c:4339 +#: ../version.c:4341 msgid "Running in Vi compatible mode" msgstr "" -#: ../version.c:4340 +#: ../version.c:4342 msgid "type :set nocp<Enter> for Vim defaults" msgstr "" -#: ../version.c:4341 +#: ../version.c:4343 msgid "type :help cp-default<Enter> for info on this" msgstr "" -#: ../version.c:4356 +#: ../version.c:4358 msgid "menu Help->Orphans for information " msgstr "" -#: ../version.c:4358 +#: ../version.c:4360 msgid "Running modeless, typed text is inserted" msgstr "" -#: ../version.c:4359 +#: ../version.c:4361 msgid "menu Edit->Global Settings->Toggle Insert Mode " msgstr "" -#: ../version.c:4360 +#: ../version.c:4362 msgid " for two modes " msgstr "" -#: ../version.c:4364 +#: ../version.c:4366 msgid "menu Edit->Global Settings->Toggle Vi Compatible" msgstr "" -#: ../version.c:4365 +#: ../version.c:4367 msgid " for Vim defaults " msgstr "" -#: ../version.c:4406 +#: ../version.c:4408 msgid "Sponsor Vim development!" msgstr "" -#: ../version.c:4407 +#: ../version.c:4409 msgid "Become a registered Vim user!" msgstr "" -#: ../version.c:4410 +#: ../version.c:4412 msgid "type :help sponsor<Enter> for information " msgstr "" -#: ../version.c:4411 +#: ../version.c:4413 msgid "type :help register<Enter> for information " msgstr "" -#: ../version.c:4413 +#: ../version.c:4415 msgid "menu Help->Sponsor/Register for information " msgstr "" diff --git a/src/testdir/samples/evil.zip b/src/testdir/samples/evil.zip new file mode 100644 index 0000000000000000000000000000000000000000..e0a7f96141eed3ec5de68babd9d8bef7c72e5845 GIT binary patch literal 148 zcmWIWW@Zs#00E(lH&MUiHn*|?*&r+i#Cm%AaFkk-te;q+TTq?{M5!rU0p5&EBFwn; h0=0v|mPQZ-HwMT+m=xg63bKlU5eQv?v=xZM005CM8qELz literal 0 HcmV?d00001 diff --git a/src/testdir/test_plugin_zip.vim b/src/testdir/test_plugin_zip.vim index 516f54e3a..08f8223b6 100644 --- a/src/testdir/test_plugin_zip.vim +++ b/src/testdir/test_plugin_zip.vim @@ -1,21 +1,23 @@ +vim9script + CheckExecutable unzip -if 0 " Find uncovered line +if 0 # Find uncovered line profile start zip_profile profile! file */zip*.vim endif runtime plugin/zipPlugin.vim -def Test_zip_basic() - - ### get our zip file - if !filecopy("samples/test.zip", "X.zip") - assert_report("Can't copy samples/test.zip") - return +def CopyZipFile(source: string) + if !filecopy($"samples/{source}", "X.zip") + assert_report($"Can't copy samples/{source}.zip") endif - defer delete("X.zip") +enddef +def g:Test_zip_basic() + CopyZipFile("test.zip") + defer delete("X.zip") e X.zip ### Check header @@ -136,15 +138,11 @@ def Test_zip_basic() bw enddef -def Test_zip_glob_fname() +def g:Test_zip_glob_fname() CheckNotMSWindows # does not work on Windows, why? - ### copy sample zip file - if !filecopy("samples/testa.zip", "X.zip") - assert_report("Can't copy samples/testa.zip") - return - endif + CopyZipFile("testa.zip") defer delete("X.zip") defer delete('zipglob', 'rf') @@ -234,14 +232,11 @@ def Test_zip_glob_fname() bw enddef -def Test_zip_fname_leading_hyphen() +def g:Test_zip_fname_leading_hyphen() CheckNotMSWindows ### copy sample zip file - if !filecopy("samples/poc.zip", "X.zip") - assert_report("Can't copy samples/poc.zip") - return - endif + CopyZipFile("poc.zip") defer delete("X.zip") defer delete('-d', 'rf') defer delete('/tmp/pwned', 'rf') @@ -256,3 +251,26 @@ def Test_zip_fname_leading_hyphen() assert_false(filereadable('/tmp/pwned')) bw enddef + +def g:Test_zip_fname_evil_path() + CheckNotMSWindows + # needed for writing the zip file + CheckExecutable zip + + CopyZipFile("evil.zip") + defer delete("X.zip") + e X.zip + + :1 + var fname = 'pwn' + search('\V' .. fname) + normal x + assert_false(filereadable('/etc/ax-pwn')) + var mess = execute(':mess') + assert_match('Path Traversal Attack', mess) + + exe ":normal \<cr>" + :w + assert_match('zipfile://.*::etc/ax-pwn', @%) + bw +enddef diff --git a/src/version.c b/src/version.c index 9fd90fa34..40c383a4b 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 */ +/**/ + 1551, /**/ 1550, /**/ -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to vim_dev+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1ubmWN-007oNe-P8%40256bit.org.