From: Christian Brabandt Date: Tue, 21 Apr 2026 19:03:02 +0000 (+0000) Subject: patch 9.2.0383: [security]: runtime(netrw): shell-injection via sftp: and file: URLs X-Git-Tag: v9.2.0383^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=405e2fb6d54d5653523809e2853d99d1c000a5fc;p=thirdparty%2Fvim.git patch 9.2.0383: [security]: runtime(netrw): shell-injection via sftp: and file: URLs Problem: runtime(netrw): shell-injection via sftp: and file: URLs (Joshua Rogers) Solution: Escape temporary file names, harden filename suffix regex, drop unused g:netrw_tmpfile_escape variable Supported by AI Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/pi_netrw.txt b/runtime/doc/pi_netrw.txt index a86cac36ba..2d98a8407b 100644 --- a/runtime/doc/pi_netrw.txt +++ b/runtime/doc/pi_netrw.txt @@ -2854,10 +2854,6 @@ your browsing preferences. (see also: |netrw-settings|) such as listing, file removal, etc. default: ssh - *g:netrw_tmpfile_escape* =' &;' - escape() is applied to all temporary files - to escape these characters. - *g:netrw_timefmt* specify format string to vim's strftime(). The default, "%c", is "the preferred date and time representation for the current diff --git a/runtime/doc/tags b/runtime/doc/tags index 720eb330f7..2b0e6cab39 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -7970,7 +7970,6 @@ g:netrw_ssh_browse_reject pi_netrw.txt /*g:netrw_ssh_browse_reject* g:netrw_ssh_cmd pi_netrw.txt /*g:netrw_ssh_cmd* g:netrw_sshport pi_netrw.txt /*g:netrw_sshport* g:netrw_timefmt pi_netrw.txt /*g:netrw_timefmt* -g:netrw_tmpfile_escape pi_netrw.txt /*g:netrw_tmpfile_escape* g:netrw_uid pi_netrw.txt /*g:netrw_uid* g:netrw_use_noswf pi_netrw.txt /*g:netrw_use_noswf* g:netrw_use_nt_rcp pi_netrw.txt /*g:netrw_use_nt_rcp* diff --git a/runtime/pack/dist/opt/netrw/autoload/netrw.vim b/runtime/pack/dist/opt/netrw/autoload/netrw.vim index e3028cb28e..538ab0377b 100644 --- a/runtime/pack/dist/opt/netrw/autoload/netrw.vim +++ b/runtime/pack/dist/opt/netrw/autoload/netrw.vim @@ -26,6 +26,8 @@ " 2026 Apr 05 by Vim Project Fix netrw#RFC2396() #19913 " 2026 Apr 15 by Vim Project Add missing escape() " 2026 Apr 19 by Vim Project expand ~ on Windows #20003 +" 2026 Apr 21 by Vim Project fix shell-injection via tempfile suffix (sftp://, file://) +" 2026 Apr 21 by Vim Project drop unused g:netrw_tmpfile_escape " Copyright: Copyright (C) 2016 Charles E. Campbell {{{1 " Permission is hereby granted to use and distribute this code, " with or without modifications, provided that this copyright @@ -400,7 +402,6 @@ else call s:NetrwInit("g:netrw_glob_escape",'*[]?`{~$\') endif call s:NetrwInit("g:netrw_menu_escape",'.&? \') -call s:NetrwInit("g:netrw_tmpfile_escape",' &;') call s:NetrwInit("s:netrw_map_escape","<|\n\r\\\\"") if has("gui_running") && (&enc == 'utf-8' || &enc == 'utf-16' || &enc == 'ucs-4') let s:treedepthstring= "│ " @@ -1821,14 +1822,14 @@ function netrw#NetRead(mode,...) "......................................... " NetRead: (sftp) NetRead Method #9 {{{3 elseif b:netrw_method == 9 - call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_sftp_cmd." ".netrw#os#Escape(g:netrw_machine.":".b:netrw_fname,1)." ".tmpfile) + call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_sftp_cmd." ".netrw#os#Escape(g:netrw_machine.":".b:netrw_fname,1)." ".netrw#os#Escape(tmpfile,1)) let result = s:NetrwGetFile(readcmd, tmpfile, b:netrw_method) let b:netrw_lastfile = choice "......................................... " NetRead: (file) NetRead Method #10 {{{3 elseif b:netrw_method == 10 && exists("g:netrw_file_cmd") - call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_file_cmd." ".netrw#os#Escape(b:netrw_fname,1)." ".tmpfile) + call netrw#os#Execute(s:netrw_silentxfer."!".g:netrw_file_cmd." ".netrw#os#Escape(b:netrw_fname,1)." ".netrw#os#Escape(tmpfile,1)) let result = s:NetrwGetFile(readcmd, tmpfile, b:netrw_method) let b:netrw_lastfile = choice @@ -8965,14 +8966,17 @@ function s:GetTempfile(fname) endif " use fname's suffix for the temporary file + " Restrict the suffix to word characters so shell metacharacters in a + " remote filename (e.g. sftp://host/foo.txt;id) cannot ride along into + " the tempfile name and out into a downstream shell command. if a:fname != "" - if a:fname =~ '\.[^./]\+$' + if a:fname =~ '\.\w\+$' if a:fname =~ '\.tar\.gz$' || a:fname =~ '\.tar\.bz2$' || a:fname =~ '\.tar\.xz$' - let suffix = ".tar".substitute(a:fname,'^.*\(\.[^./]\+\)$','\1','e') + let suffix = ".tar".substitute(a:fname,'^.*\(\.\w\+\)$','\1','e') elseif a:fname =~ '.txz$' - let suffix = ".txz".substitute(a:fname,'^.*\(\.[^./]\+\)$','\1','e') + let suffix = ".txz".substitute(a:fname,'^.*\(\.\w\+\)$','\1','e') else - let suffix = substitute(a:fname,'^.*\(\.[^./]\+\)$','\1','e') + let suffix = substitute(a:fname,'^.*\(\.\w\+\)$','\1','e') endif let tmpfile= substitute(tmpfile,'\.tmp$','','e') let tmpfile .= suffix diff --git a/runtime/pack/dist/opt/netrw/doc/netrw.txt b/runtime/pack/dist/opt/netrw/doc/netrw.txt index 01a5bda597..144bab5fb3 100644 --- a/runtime/pack/dist/opt/netrw/doc/netrw.txt +++ b/runtime/pack/dist/opt/netrw/doc/netrw.txt @@ -2854,10 +2854,6 @@ your browsing preferences. (see also: |netrw-settings|) such as listing, file removal, etc. default: ssh - *g:netrw_tmpfile_escape* =' &;' - escape() is applied to all temporary files - to escape these characters. - *g:netrw_timefmt* specify format string to vim's strftime(). The default, "%c", is "the preferred date and time representation for the current diff --git a/src/testdir/test_plugin_netrw.vim b/src/testdir/test_plugin_netrw.vim index 6ee731d88f..893e424362 100644 --- a/src/testdir/test_plugin_netrw.vim +++ b/src/testdir/test_plugin_netrw.vim @@ -604,6 +604,36 @@ func Test_netrw_FileUrlEdit_pipe_injection() call assert_false(filereadable(fname), 'Command injection via pipe in file URL') endfunc +" The remote filename after '.' was allowed to contain shell metacharacters +" and rode unescaped into the tempfile name passed to sftp/file_cmd, giving a +" shell injection on :e sftp://host/foo.txt;. +func Test_netrw_tempfile_suffix_injection() + CheckUnix + CheckExecutable id + let save_sftp = g:netrw_sftp_cmd + let save_file = exists('g:netrw_file_cmd') ? g:netrw_file_cmd : v:null + let g:netrw_sftp_cmd = 'true' + let g:netrw_file_cmd = 'true' + let fname = 'Xrce_marker' + try + call delete(fname) + sil! call netrw#NetRead(2, 'sftp://localhost/foo.txt;id>'..fname) + call assert_false(filereadable(fname), 'Command injection via sftp:// tempfile suffix') + + call delete(fname) + sil! call netrw#NetRead(2, 'file://localhost/foo.txt;id>'..fname) + call assert_false(filereadable(fname), 'Command injection via file:// tempfile suffix') + finally + call delete(fname) + let g:netrw_sftp_cmd = save_sftp + if save_file is v:null + unlet! g:netrw_file_cmd + else + let g:netrw_file_cmd = save_file + endif + endtry +endfunc + func Test_netrw_RFC2396() let fname = 'a%20b' call assert_equal('a b', netrw#RFC2396(fname)) diff --git a/src/version.c b/src/version.c index 3f85f2f579..895c22e9c0 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 383, /**/ 382, /**/