5 Commits
Author SHA1 Message Date
Junegunn Choi 9705771895 Run the tag address in a sandbox in the preview too
A preview runs as soon as an entry is highlighted, so a crafted tags file did
not even need to be selected to get a shell command out of
'vim -c "silent {excmd}"'.

The preview Vim starts with '-u NONE' and exits right after, so the sandbox
alone is enough there; nothing it still permits outlives the process.
2026-09-27 23:20:27 +09:00
Junegunn Choi c228c70ee3 Allow an unterminated pattern after earlier address parts
A field holding a tab arrives cut short, so :BTags with 'ctags
--excmd=combine' sees '2;/^int tabbed(void) {' and stopped jumping. The last
part runs to the end of the line, taking any '|' or ';' with it, so it still
cannot chain a command.
2026-09-27 21:37:04 +09:00
Junegunn Choi a5717367fc Do not add a quickfix item for a refused tag address
The cursor has not moved, so the item repeated the previous entry's line and
text. :BTags already skips such an entry.
2026-09-27 21:24:52 +09:00
Junegunn Choi 88b0ae41c8 Check the address on the BTags paths too
The sink and the CTRL-O Show callback passed the raw field into the Ex command.
A custom tag command can emit any address, so put it through the same check as
:Tags. The BTags source aligns its columns, hence the strip.
2026-09-27 20:59:01 +09:00
Junegunn Choi d538da05b6 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 left unterminated runs to the end of the line, taking any '|' or
  ';' with it, so it cannot chain a command
- Run the address in a sandbox, as builtin tag jumps do since Vim 6.0

Close #1626
2026-09-27 20:52:29 +09:00
2 changed files with 45 additions and 7 deletions
+41 -6
View File
@@ -534,6 +534,30 @@ function! s:execute_silent(cmd)
silent keepjumps keepalt execute a:cmd
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
function! s:execute_tag_address(excmd)
silent keepjumps keepalt sandbox execute a:excmd
endfunction
" Address of a tag, given the rest of the line after the file name. 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. The last pattern can be left unterminated, as a
" field holding a tab arrives cut short; it then runs to the end of the line,
" taking any '|' or ';' with it, so it cannot chain a command. Returns an
" empty string for anything else, which is then not run.
let s:tag_address_part = '\%(\d\+\|/\%(\\.\|[^/\\]\)*/\|?\%(\\.\|[^?\\]\)*?\)'
let s:tag_address_open = '\%(/\%(\\.\|[^/\\]\)*\|?\%(\\.\|[^?\\]\)*\)'
function! s:tag_address(rest)
" :BTags aligns its columns, so its field arrives padded
let rest = s:strip(a:rest)
let address = matchstr(rest,
\ '^'.s:tag_address_part.'\%(;'.s:tag_address_part.'\)*\ze\%(;"\|$\)')
return empty(address) ? matchstr(rest,
\ '^\%('.s:tag_address_part.';\)*'.s:tag_address_open.'$') : address
endfunction
" [key, [filename, [stay_on_edit: 0]]]
function! s:action_for(key, ...)
let Cmd = get(get(g:, 'fzf_action', s:default_action), a:key, '')
@@ -729,7 +753,7 @@ function! s:goto_entry(winid, bufnr, dir, kind, entry) abort
if len(parts) < 3
return
endif
let excmd = matchstr(join(parts[2:-2], '')[:-2], '^.\{-}\ze;\?"\t')
let excmd = s:tag_address(join(parts[2:-2], '')[:-2])
let relpath = parts[1][:-2]
let path = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/')
\ ? relpath : join([fnamemodify(parts[-1], ':h'), relpath], '/')
@@ -738,14 +762,18 @@ function! s:goto_entry(winid, bufnr, dir, kind, entry) abort
return
endif
let cmds = s:edit_cmds(a:winid, path)
\ + ['keepjumps '.excmd, 'normal! ^zvzz']
\ + ['sandbox keepjumps '.excmd, 'normal! ^zvzz']
elseif a:kind ==# 'btags'
let parts = split(entry, "\t")
if len(parts) < 3 || !bufexists(a:bufnr)
return
endif
let excmd = s:tag_address(parts[2])
if empty(excmd)
return
endif
let cmds = ['keepalt keepjumps hide buffer '.a:bufnr,
\ 'keepjumps '.parts[2], 'normal! zvzz']
\ 'sandbox keepjumps '.excmd, 'normal! zvzz']
elseif a:kind ==# 'marks'
" A lowercase mark is local to the buffer the run started on
let mark = matchstr(entry, '^\s*\zs\S')
@@ -1654,7 +1682,11 @@ function! s:btags_sink(from, lines)
let qfl = []
for line in a:lines[1:]
let parts = split(line, "\t")
call s:execute_silent(parts[2])
let excmd = s:tag_address(parts[2])
if empty(excmd)
continue
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])
@@ -1724,7 +1756,7 @@ function! s:tags_sink(from, lines)
for line in list
try
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])
let base = fnamemodify(parts[-1], ':h')
let relpath = parts[1][:-2]
let abspath = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/') ? relpath : join([base, relpath], '/')
@@ -1734,7 +1766,10 @@ function! s:tags_sink(from, lines)
else
call s:open(expand(abspath, 1))
endif
call s:execute_silent(excmd)
if empty(excmd)
continue
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])
+4 -1
View File
@@ -30,9 +30,12 @@ else
exit 1
fi
# The address comes from a tags file, which can be untrusted, and a preview
# runs as soon as an entry is highlighted. Sandboxed as a builtin tag jump is,
# so that it cannot run a shell command or touch a file
CENTER="$("${VIMNAME}" -R -i NONE -u NONE -e -m -s "${FILE}" \
-c "set nomagic" \
-c "silent ${EXCMD}" \
-c "silent sandbox ${EXCMD}" \
-c 'let l=line(".") | new | put =l | print | qa!')" || exit
START_LINE="$(( CENTER - FZF_PREVIEW_LINES / 2 ))"