patch 9.2.0678: [security]: potential powershell code execution in zip.vim

Commit: 
https://github.com/vim/vim/commit/b2cc9be119d51212bf0d3f2a994c7e517c73f4a9
Author: Christian Brabandt <[email protected]>
Date:   Sat Jun 20 15:35:58 2026 +0000

    patch 9.2.0678: [security]: potential powershell code execution in zip.vim
    
    Problem:  [security]: potential powershell code execution in zip.vim
              (DDugs)
    Solution: Cleanup zip.vim, introduce PSEscape() to escape() potential 
powershell code,
              use consistent s:Escape() in the various PowerShell functions
    
    Github Security Advisory:
    https://github.com/vim/vim/security/advisories/GHSA-x5fg-h5w9-9frf
    
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/autoload/zip.vim b/runtime/autoload/zip.vim
index aad548239..44bdfc682 100644
--- a/runtime/autoload/zip.vim
+++ b/runtime/autoload/zip.vim
@@ -24,6 +24,7 @@
 " 2026 Apr 05 by Vim Project: Detect more path traversal attacks
 " 2026 Apr 14 by Vim Project: Detect more path traversal attacks on Windows
 " 2026 Apr 15 by Vim Project: Detect more path traversal attacks on Windows
+" 2026 Jun 20 by Vim Project: Fix wrong escaping for the powershell calls
 " 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,
@@ -51,15 +52,6 @@ let s:NOTE           = 0
 
 " ---------------------------------------------------------------------
 "  Global Values: {{{1
-if !exists("g:zip_shq")
- if &shq != ""
-  let g:zip_shq= &shq
- elseif has("unix")
-  let g:zip_shq= "'"
- else
-  let g:zip_shq= '"'
- endif
-endif
 if !exists("g:zip_zipcmd")
  let g:zip_zipcmd= "zip"
 endif
@@ -135,7 +127,7 @@ function! s:ZipBrowsePS(zipfile)
   " Browse the contents of a zip file using PowerShell's
   " Equivalent `unzip -Z1 -- zipfile`
   let cmds = [
-        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:Escape(a:zipfile, 1) . ');',
+        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:PSEscape(a:zipfile) . ');',
         \ '$zip.Entries | ForEach-Object { $_.FullName };',
         \ '$zip.Dispose()'
         \ ]
@@ -149,16 +141,16 @@ function! s:ZipReadPS(zipfile, fname, tempfile)
     call s:Mess('WarningMsg', "***warning*** PowerShell can display, but 
cannot update, files in archive subfolders")
   endif
   let cmds = [
-        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:Escape(a:zipfile, 1) . ');',
-        \ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . 
s:Escape(a:fname, 1) . ' };',
+        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:PSEscape(a:zipfile) . ');',
+        \ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . 
s:PSEscape(a:fname) . ' };',
         \ '$stream = $fileEntry.Open();',
-        \ '$fileStream = [System.IO.File]::Create(' . s:Escape(a:tempfile, 1) 
. ');',
+        \ '$fileStream = [System.IO.File]::Create(' . s:PSEscape(a:tempfile) . 
');',
         \ '$stream.CopyTo($fileStream);',
         \ '$fileStream.Close();',
         \ '$stream.Close();',
         \ '$zip.Dispose()'
         \ ]
-  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
 endfunction
 
 function! s:ZipUpdatePS(zipfile, fname)
@@ -168,7 +160,7 @@ function! s:ZipUpdatePS(zipfile, fname)
     call s:Mess('Error', "***error*** PowerShell cannot update files in 
archive subfolders")
     return ':'
   endif
-  return 'Compress-Archive -Path ' . a:fname . ' -Update -DestinationPath ' . 
a:zipfile
+  return 'Compress-Archive -Path ' . s:PSEscape(a:fname) . ' -Update 
-DestinationPath ' . s:PSEscape(a:zipfile)
 endfunction
 
 function! s:ZipExtractFilePS(zipfile, fname)
@@ -179,16 +171,16 @@ function! s:ZipExtractFilePS(zipfile, fname)
     return ':'
   endif
   let cmds = [
-        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:Escape(a:zipfile, 1) . ');',
-        \ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . 
a:fname . ' };',
+        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:PSEscape(a:zipfile) . ');',
+        \ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . 
s:PSEscape(a:fname) . ' };',
         \ '$stream = $fileEntry.Open();',
-        \ '$fileStream = [System.IO.File]::Create(' . a:fname . ');',
+        \ '$fileStream = [System.IO.File]::Create(' . s:PSEscape(a:fname) . 
');',
         \ '$stream.CopyTo($fileStream);',
         \ '$fileStream.Close();',
         \ '$stream.Close();',
         \ '$zip.Dispose()'
         \ ]
-  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
 endfunction
 
 function! s:ZipDeleteFilePS(zipfile, fname)
@@ -196,12 +188,12 @@ function! s:ZipDeleteFilePS(zipfile, fname)
   " Equivalent to `zip -d zipfile fname`
   let cmds = [
         \ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
-        \ '$zip = [System.IO.Compression.ZipFile]::Open(' . 
s:Escape(a:zipfile, 1) . ', ''Update'');',
-        \ '$entry = $zip.Entries | Where-Object { $_.Name -eq ' . 
s:Escape(a:fname, 1) . ' };',
+        \ '$zip = [System.IO.Compression.ZipFile]::Open(' . 
s:PSEscape(a:zipfile) . ', ''Update'');',
+        \ '$entry = $zip.Entries | Where-Object { $_.Name -eq ' . 
s:PSEscape(a:fname) . ' };',
         \ 'if ($entry) { $entry.Delete(); $zip.Dispose() }',
         \ 'else { $zip.Dispose() }'
         \ ]
-  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '))
 endfunction
 
 " ----------------
@@ -341,9 +333,9 @@ fun! zip#Read(fname,mode)
   let temp = tempname()
   let fn   = expand('%:p')
 
-  let gnu_cmd = g:zip_unzipcmd . ' -p -- ' . s:Escape(zipfile, 0) . ' ' . 
s:Escape(fname, 0) . ' > ' . s:Escape(temp, 0)
-  let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . ''')'
-  let ps_cmd = 'sil !' . s:ZipReadPS(zipfile, fname, temp)
+  let gnu_cmd = g:zip_unzipcmd . ' -p -- ' . s:Escape(zipfile) . ' ' . 
s:Escape(fname) . ' > ' . s:Escape(temp)
+  let gnu_cmd = 'call system(' . string(gnu_cmd) . ')'
+  let ps_cmd = 'call system(' . string(s:ZipReadPS(zipfile, fname, temp)) . ')'
   call s:TryExecGnuFallBackToPs(g:zip_unzipcmd, gnu_cmd, ps_cmd)
 
   sil exe 'keepalt file '.temp
@@ -415,9 +407,9 @@ fun! zip#Write(fname)
     endif
   endif
   if fname =~ '^[.]\{1,2}/'
-    let gnu_cmd = g:zip_zipcmd . ' -d ' . 
s:Escape(fnamemodify(zipfile,":p"),0) . ' ' . s:Escape(fname,0)
-    let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . 
''')'
-    let ps_cmd = $"call system({s:Escape(s:ZipDeleteFilePS(zipfile, fname), 
1)})"
+    let gnu_cmd = g:zip_zipcmd . ' -d ' . s:Escape(fnamemodify(zipfile,":p")) 
. ' ' . s:Escape(fname)
+    let gnu_cmd = 'call system(' . string(gnu_cmd) . ')'
+    let ps_cmd = $"call system({string(s:ZipDeleteFilePS(zipfile, fname))})"
     call s:TryExecGnuFallBackToPs(g:zip_zipcmd, gnu_cmd, ps_cmd)
     let fname = fname->substitute('^\([.]\{1,2}/\)\+', '', 'g')
     let need_rename = 1
@@ -426,7 +418,7 @@ fun! zip#Write(fname)
   if fname =~ '/'
     let dirpath = substitute(fname,'/[^/]\+$','','e')
     if has("win32unix") && executable("cygpath")
-    let dirpath = substitute(system("cygpath ".s:Escape(dirpath,0)),'
','','e')
+    let dirpath = substitute(system("cygpath ".s:Escape(dirpath)),'
','','e')
     endif
     call mkdir(dirpath,"p")
   endif
@@ -437,16 +429,17 @@ fun! zip#Write(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)),'
','','e')
   endif
 
   if (has("win32") || has("win95") || has("win64") || has("win16")) && &shell 
!~? 'sh$'
     let fname = substitute(fname, '[', '[[]', 'g')
   endif
 
-  let gnu_cmd = g:zip_zipcmd . ' -u '. s:Escape(fnamemodify(zipfile,":p"),0) . 
' ' . s:Escape(fname,0)
+  let gnu_cmd = g:zip_zipcmd . ' -u '. s:Escape(fnamemodify(zipfile,":p")) . ' 
' . s:Escape(fname)
   let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . ''')'
-  let ps_cmd = s:ZipUpdatePS(s:Escape(fnamemodify(zipfile, ':p'), 0), 
s:Escape(fname, 0))
+  let zip = fnamemodify(zipfile, ':p')
+  let ps_cmd = s:ZipUpdatePS(zip, fname)
   let ps_cmd = 'call system(''' . substitute(ps_cmd, "'", "''", 'g') . ''')'
   call s:TryExecGnuFallBackToPs(g:zip_zipcmd, gnu_cmd, ps_cmd)
   if &shell =~ 'pwsh'
@@ -541,8 +534,8 @@ fun! zip#Extract()
 
   " extract the file mentioned under the cursor
   let gnu_cmd = g:zip_extractcmd . ' -o '. shellescape(b:zipfile) . ' ' . 
target
-  let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . ''')'
-  let ps_cmd = $"call system({s:Escape(s:ZipExtractFilePS(b:zipfile, target), 
1)})"
+  let gnu_cmd = 'call system(' . string(gnu_cmd) . ')'
+  let ps_cmd = 'call system(' . string(s:ZipExtractFilePS(b:zipfile, fname)) . 
')'
   call s:TryExecGnuFallBackToPs(g:zip_extractcmd, gnu_cmd, ps_cmd)
 
   if v:shell_error != 0
@@ -556,19 +549,20 @@ endfun
 
 " ---------------------------------------------------------------------
 " s:Escape: {{{2
-fun! s:Escape(fname,isfilt)
-  if exists("*shellescape")
-   if a:isfilt
-    let qnameq= shellescape(a:fname,1)
-   else
-    let qnameq= shellescape(a:fname)
-   endif
+fun! s:Escape(fname, isfilt = 0)
+  if a:isfilt
+   let qnameq = shellescape(a:fname, 1)
   else
-   let qnameq= g:zip_shq.escape(a:fname,g:zip_shq).g:zip_shq
+   let qnameq = shellescape(a:fname)
   endif
   return qnameq
 endfun
 
+" s:PSEscape: Escape a string for Powershell, shellescape() does not work here 
{{{2
+fun! s:PSEscape(str)
+  return "'" .. substitute(a:str, "'", "''", 'g') .. "'"
+endfun
+
 " ---------------------------------------------------------------------
 " s:ChgDir: {{{2
 fun! s:ChgDir(newdir,errlvl,errmsg)
diff --git a/runtime/doc/pi_zip.txt b/runtime/doc/pi_zip.txt
index 81275b24e..dc462cfef 100644
--- a/runtime/doc/pi_zip.txt
+++ b/runtime/doc/pi_zip.txt
@@ -1,4 +1,4 @@
-*pi_zip.txt*   For Vim version 9.2.  Last change: 2026 May 16
+*pi_zip.txt*   For Vim version 9.2.  Last change: 2026 Jun 20
 
                                +====================+
                                | Zip File Interface |
@@ -48,16 +48,6 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell        
 *zip-copyright*
    If this variable exists and is true, the file window will not be
    automatically maximized when opened.
 
-                                                       *g:zip_shq*
-   Different operating systems may use one or more shells to execute
-   commands.  Zip will try to guess the correct quoting mechanism to
-   allow spaces and whatnot in filenames; however, if it is incorrectly
-   guessing the quote to use for your setup, you may use >
-       g:zip_shq
-<   which by default is a single quote under Unix (') and a double quote
-   under Windows (").  If you'd rather have no quotes, simply set
-   g:zip_shq to the empty string (let g:zip_shq= "") in your <.vimrc>.
-
                                                        *g:zip_unzipcmd*
    Use this option to specify the program which does the duty of "unzip".
    It's used during browsing. By default: >
diff --git a/src/version.c b/src/version.c
index d2e01aecb..9de098d71 100644
--- a/src/version.c
+++ b/src/version.c
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    678,
 /**/
     677,
 /**/

-- 
-- 
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 [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1way71-007VsM-Ri%40256bit.org.

Raspunde prin e-mail lui