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
Junegunn Choi a630ef91e1 Fix UTF-8 corruption when pasting at multibyte character
col('.') is the first byte of the character under the cursor, so splitting
the line there left one byte in head and the rest in tail. Advance past the
whole character instead.

Close #1627
2026-09-26 17:04:35 +09:00
Junegunn Choi 8a0068127a Add g:fzf_vim.options for fzf options shared by all commands (#1624)
- Key hint footer had no off switch. $FZF_DEFAULT_OPTS could not give one,
  since fzf reads it before the options fzf.vim passes
- Merged ahead of the per-call and command-level options, so both still
  take precedence
- '--no-footer' there hides the hints. Documented as fzf-vim-key-hints,
  the footer had no name to look up
- Help TOC also gained Paste key and Show key, missing since those
  sections were added

Close #1623
2026-09-07 11:39:02 +09:00
2 changed files with 49 additions and 8 deletions
+45 -7
View File
@@ -498,7 +498,10 @@ function! fzf#vim#paste(items) abort
return s:warn('Cannot paste into a nomodifiable buffer')
endif
let line = getline('.')
let idx = empty(line) ? 0 : col('.')
" col('.') is the first byte of the character under the cursor. Skip the
" whole character so that a multibyte character is not split in half.
let cursor = col('.') - 1
let idx = empty(line) ? 0 : cursor + strlen(matchstr(line, '.', cursor))
let head = strpart(line, 0, idx)
let tail = strpart(line, idx)
let pad = (!empty(head) && head !~ '\s$') ? ' ' : ''
@@ -531,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, '')
@@ -726,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], '/')
@@ -735,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')
@@ -1651,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])
@@ -1721,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], '/')
@@ -1731,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 ))"