mirror of
https://github.com/junegunn/fzf.vim.git
synced 2026-10-09 23:05:52 +08:00
Do not run arbitrary Ex commands from tag addresses
A tags file can come from an untrusted source, and its address field can hold any Ex command, which :Tags, :BTags and their CTRL-O Show callback ran as is. - Take only a line number or a search pattern, chained with ';' for '--excmd=combine' and cut at the ';"' terminator as Vim's find_extra() does. A pattern can be left unterminated, which takes any '|' or ';' into the pattern, so it cannot chain a command either. Anything else is refused by name - Run the address in a sandbox, as builtin tag jumps do since Vim 6.0. ':sandbox' as a modifier stops at the first '|', so the address goes to ':execute', which keeps all of it inside - :BTags gains the search settings and the per-entry recovery the other two paths already had, so one failing address no longer drops the rest Close #1626
This commit is contained in:
1 file changed
+84
-14
+84
-14
@@ -534,6 +534,49 @@ function! s:execute_silent(cmd)
|
|||||||
silent keepjumps keepalt execute a:cmd
|
silent keepjumps keepalt execute a:cmd
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" A tag address can be any Ex command, and a tags file can come from an
|
||||||
|
" untrusted source, so run it in a sandbox as a builtin tag jump does. ':sandbox'
|
||||||
|
" as a modifier only covers the command up to the first '|', so hand the address
|
||||||
|
" to ':execute', which keeps all of it inside the sandbox
|
||||||
|
function! s:execute_tag_address(excmd)
|
||||||
|
silent keepjumps keepalt sandbox execute a:excmd
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" The same, as a command for a win_execute() list
|
||||||
|
function! s:sandboxed(cmd)
|
||||||
|
return 'sandbox execute '.string(a:cmd)
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
" Address of a tag, given the rest of the line after the file name, or, with
|
||||||
|
" 'whole' off, the address field alone as :BTags reads it. Only the forms a tags
|
||||||
|
" file is meant to hold, a line number and a search pattern, chained with ';' for
|
||||||
|
" '--excmd=combine' and cut at the ';"' terminator as Vim's find_extra() does.
|
||||||
|
" Returns an empty string for anything else, which is then not run.
|
||||||
|
"
|
||||||
|
" A pattern holding a tab reaches :BTags cut short. Where earlier parts parsed,
|
||||||
|
" keep those, since a line number beats half a pattern. A fragment on its own is
|
||||||
|
" an address only where the whole line is in hand, as in a helptags file, whose
|
||||||
|
" '/*:Ag*' carries no terminator. ctags always terminates, so on the field alone
|
||||||
|
" a fragment is the cut, and it would match some other line. Running to the end
|
||||||
|
" of the line it takes any '|' or ';' with it, so it cannot chain a command.
|
||||||
|
let s:tag_address_part = '\%(\d\+\|/\%(\\.\|[^/\\]\)*/\|?\%(\\.\|[^?\\]\)*?\)'
|
||||||
|
let s:tag_address_open = '\%(/\%(\\.\|[^/\\\t]\)*\|?\%(\\.\|[^?\\\t]\)*\)'
|
||||||
|
function! s:tag_address(rest, whole)
|
||||||
|
" :BTags aligns its columns, so its field arrives padded, and a tags file with
|
||||||
|
" CRLF endings leaves a carriage return, which s:strip() keeps
|
||||||
|
let rest = substitute(a:rest, '^[ \t\r]*\|[ \t\r]*$', '', 'g')
|
||||||
|
let chain = '^'.s:tag_address_part.'\%(;'.s:tag_address_part.'\)*'
|
||||||
|
let address = matchstr(rest, chain.'\ze\%(;"\|$\)')
|
||||||
|
if !empty(address)
|
||||||
|
return address
|
||||||
|
endif
|
||||||
|
let address = matchstr(rest, chain.'\ze;[/?]')
|
||||||
|
if !empty(address)
|
||||||
|
return address
|
||||||
|
endif
|
||||||
|
return a:whole ? matchstr(rest, '^'.s:tag_address_open.'\ze\%(\t\|$\)') : ''
|
||||||
|
endfunction
|
||||||
|
|
||||||
" [key, [filename, [stay_on_edit: 0]]]
|
" [key, [filename, [stay_on_edit: 0]]]
|
||||||
function! s:action_for(key, ...)
|
function! s:action_for(key, ...)
|
||||||
let Cmd = get(get(g:, 'fzf_action', s:default_action), a:key, '')
|
let Cmd = get(get(g:, 'fzf_action', s:default_action), a:key, '')
|
||||||
@@ -729,23 +772,30 @@ function! s:goto_entry(winid, bufnr, dir, kind, entry) abort
|
|||||||
if len(parts) < 3
|
if len(parts) < 3
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
let excmd = matchstr(join(parts[2:-2], '')[:-2], '^.\{-}\ze;\?"\t')
|
let excmd = s:tag_address(join(parts[2:-2], '')[:-2], 1)
|
||||||
|
if empty(excmd)
|
||||||
|
return s:warn('Unsupported tag address: '.s:strip(parts[0]))
|
||||||
|
endif
|
||||||
let relpath = parts[1][:-2]
|
let relpath = parts[1][:-2]
|
||||||
let path = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/')
|
let path = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/')
|
||||||
\ ? relpath : join([fnamemodify(parts[-1], ':h'), relpath], '/')
|
\ ? relpath : join([fnamemodify(parts[-1], ':h'), relpath], '/')
|
||||||
let path = expand(path, 1)
|
let path = expand(path, 1)
|
||||||
if empty(excmd) || !filereadable(path)
|
if !filereadable(path)
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
let cmds = s:edit_cmds(a:winid, path)
|
let cmds = s:edit_cmds(a:winid, path)
|
||||||
\ + ['keepjumps '.excmd, 'normal! ^zvzz']
|
\ + [s:sandboxed('keepjumps '.excmd), 'normal! ^zvzz']
|
||||||
elseif a:kind ==# 'btags'
|
elseif a:kind ==# 'btags'
|
||||||
let parts = split(entry, "\t")
|
let parts = split(entry, "\t")
|
||||||
if len(parts) < 3 || !bufexists(a:bufnr)
|
if len(parts) < 3 || !bufexists(a:bufnr)
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
|
let excmd = s:tag_address(parts[2], 0)
|
||||||
|
if empty(excmd)
|
||||||
|
return s:warn('Unsupported tag address: '.s:strip(parts[0]))
|
||||||
|
endif
|
||||||
let cmds = ['keepalt keepjumps hide buffer '.a:bufnr,
|
let cmds = ['keepalt keepjumps hide buffer '.a:bufnr,
|
||||||
\ 'keepjumps '.parts[2], 'normal! zvzz']
|
\ s:sandboxed('keepjumps '.excmd), 'normal! zvzz']
|
||||||
elseif a:kind ==# 'marks'
|
elseif a:kind ==# 'marks'
|
||||||
" A lowercase mark is local to the buffer the run started on
|
" A lowercase mark is local to the buffer the run started on
|
||||||
let mark = matchstr(entry, '^\s*\zs\S')
|
let mark = matchstr(entry, '^\s*\zs\S')
|
||||||
@@ -1652,14 +1702,31 @@ function! s:btags_sink(from, lines)
|
|||||||
let tagname = ''
|
let tagname = ''
|
||||||
call s:action_for(a:lines[0])
|
call s:action_for(a:lines[0])
|
||||||
let qfl = []
|
let qfl = []
|
||||||
for line in a:lines[1:]
|
try
|
||||||
let parts = split(line, "\t")
|
" Searched with the settings s:tags_sink and the CTRL-O callback use, and a
|
||||||
call s:execute_silent(parts[2])
|
" failing address must not drop the rest of the selection
|
||||||
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
|
let [magic, &magic, wrapscan, &wrapscan] = [&magic, 0, &wrapscan, 1]
|
||||||
if empty(tagname)
|
for line in a:lines[1:]
|
||||||
let tagname = s:strip(parts[0])
|
try
|
||||||
endif
|
let parts = split(line, "\t")
|
||||||
endfor
|
let excmd = len(parts) > 2 ? s:tag_address(parts[2], 0) : ''
|
||||||
|
if empty(excmd)
|
||||||
|
throw 'Unsupported tag address: '.s:strip(get(parts, 0, line))
|
||||||
|
endif
|
||||||
|
call s:execute_tag_address(excmd)
|
||||||
|
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
|
||||||
|
if empty(tagname)
|
||||||
|
let tagname = s:strip(parts[0])
|
||||||
|
endif
|
||||||
|
catch /^Vim:Interrupt$/
|
||||||
|
break
|
||||||
|
catch
|
||||||
|
call s:warn(v:exception)
|
||||||
|
endtry
|
||||||
|
endfor
|
||||||
|
finally
|
||||||
|
let [&magic, &wrapscan] = [magic, wrapscan]
|
||||||
|
endtry
|
||||||
|
|
||||||
if len(qfl) > 1
|
if len(qfl) > 1
|
||||||
" Go back to the original position
|
" Go back to the original position
|
||||||
@@ -1724,7 +1791,10 @@ function! s:tags_sink(from, lines)
|
|||||||
for line in list
|
for line in list
|
||||||
try
|
try
|
||||||
let parts = split(line, '\t\zs')
|
let parts = split(line, '\t\zs')
|
||||||
let excmd = matchstr(join(parts[2:-2], '')[:-2], '^.\{-}\ze;\?"\t')
|
let excmd = s:tag_address(join(parts[2:-2], '')[:-2], 1)
|
||||||
|
if empty(excmd)
|
||||||
|
throw 'Unsupported tag address: '.s:strip(get(parts, 0, line))
|
||||||
|
endif
|
||||||
let base = fnamemodify(parts[-1], ':h')
|
let base = fnamemodify(parts[-1], ':h')
|
||||||
let relpath = parts[1][:-2]
|
let relpath = parts[1][:-2]
|
||||||
let abspath = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/') ? relpath : join([base, relpath], '/')
|
let abspath = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/') ? relpath : join([base, relpath], '/')
|
||||||
@@ -1734,7 +1804,7 @@ function! s:tags_sink(from, lines)
|
|||||||
else
|
else
|
||||||
call s:open(expand(abspath, 1))
|
call s:open(expand(abspath, 1))
|
||||||
endif
|
endif
|
||||||
call s:execute_silent(excmd)
|
call s:execute_tag_address(excmd)
|
||||||
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
|
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
|
||||||
if empty(tagname)
|
if empty(tagname)
|
||||||
let tagname = s:strip(parts[0])
|
let tagname = s:strip(parts[0])
|
||||||
|
|||||||
Reference in new issue
Block a user