Commit: patch 9.2.0383: [security]: runtime(netrw): shell-injection via sftp: and file: URLs

4 views
Skip to first unread message

Christian Brabandt

unread,
Apr 21, 2026, 3:15:15 PMApr 21
to vim...@googlegroups.com
patch 9.2.0383: [security]: runtime(netrw): shell-injection via sftp: and file: URLs

Commit: https://github.com/vim/vim/commit/405e2fb6d54d5653523809e2853d99d1c000a5fc
Author: Christian Brabandt <c...@256bit.org>
Date: Tue Apr 21 19:03:02 2026 +0000

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 <c...@256bit.org>

diff --git a/runtime/doc/pi_netrw.txt b/runtime/doc/pi_netrw.txt
index a86cac36b..2d98a8407 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 720eb330f..2b0e6cab3 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 e3028cb28..538ab0377 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","<|

\\<C-V>\"")
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,'^.*\(\.[^./]\+\)$',' ','e')
+ let suffix = ".tar".substitute(a:fname,'^.*\(\.\w\+\)$',' ','e')
elseif a:fname =~ '.txz$'
- let suffix = ".txz".substitute(a:fname,'^.*\(\.[^./]\+\)$',' ','e')
+ let suffix = ".txz".substitute(a:fname,'^.*\(\.\w\+\)$',' ','e')
else
- let suffix = substitute(a:fname,'^.*\(\.[^./]\+\)$',' ','e')
+ let suffix = substitute(a:fname,'^.*\(\.\w\+\)$',' ','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 01a5bda59..144bab5fb 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 6ee731d88..893e42436 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;<cmd>.
+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 3f85f2f57..895c22e9c 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,
/**/
Reply all
Reply to author
Forward
0 new messages