From b2cc9be119d51212bf0d3f2a994c7e517c73f4a9 Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Sat, 20 Jun 2026 15:35:58 +0000 Subject: [PATCH] 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 --- runtime/autoload/zip.vim | 78 +++++++++++++++++++--------------------- runtime/doc/pi_zip.txt | 12 +------ src/version.c | 2 ++ 3 files changed, 39 insertions(+), 53 deletions(-) diff --git a/runtime/autoload/zip.vim b/runtime/autoload/zip.vim index aad548239a..44bdfc6820 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)),'\n','','e') + let dirpath = substitute(system("cygpath ".s:Escape(dirpath)),'\n','','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)),'\n','','e') + let zipfile = substitute(system("cygpath ".s:Escape(zipfile)),'\n','','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 81275b24ec..dc462cfefc 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 d2e01aecbc..9de098d718 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, /**/ -- 2.47.3