Files
neovim/test/old/testdir/test_plugin_ccomplete.vim
zeertzjq 72b2c713ea vim-patch:9.2.0845: [security]: arbitrary Ex command execution during C omni-completion
Problem:  [security]: arbitrary Ex command execution during C
          omni-completion (Threonine)
Solution: Match tags typeref literally to block Ex command injection
          (Yasuhiro Matsumoto).

Escaping only "/" and "\" left the typeref able to break out of the
:vimgrep pattern without a "/": an unclosed "[" makes vimgrep's pattern
skipping fail, and the parser then treats a following "|" as a command
separator, so the tag value runs as Ex commands during C omni-completion.
Match the field literally with \V so no regex metacharacter can affect
pattern parsing.

Github Security Advisory:
https://github.com/vim/vim/security/advisories/GHSA-cx73-phcg-3j5g

2f628d8104

Co-authored-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
2026-07-25 06:23:13 +08:00

89 lines
2.8 KiB
VimL

" Tests for the C omni-completion plugin (runtime/autoload/ccomplete.vim).
func s:WriteTags(lines)
" Mark unsorted so lookup is a linear scan regardless of entry order.
let tagsfile = tempname()
call writefile(["!_TAG_FILE_SORTED\t0\t/0/"] + a:lines, tagsfile)
return tagsfile
endfunc
" A crafted typeref field is interpolated into the :vimgrep pattern in
" StructMembers(). Without escaping, "/" closes the pattern and "|" starts a
" new Ex command, so the field runs as an Ex command during completion.
func Test_ccomplete_no_exec_via_typeref()
unlet! g:ccomplete_injected
let tagsfile = s:WriteTags([
\ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:x/|let g:ccomplete_injected = 1|\"",
\ ])
let save_tags = &tags
let &tags = tagsfile
new
call ccomplete#Complete(1, '')
call ccomplete#Complete(0, 'myvar.x')
call assert_false(exists('g:ccomplete_injected'),
\ 'typeref field was executed as an Ex command during omni-completion')
bwipe!
let &tags = save_tags
unlet! g:ccomplete_injected
endfunc
" Escaping "/" and "\" is not enough: with no "/" in the payload, an unclosed
" "[" makes vimgrep's pattern skipping fail, and the command parser then treats
" the first "|" as a command separator. The typeref must be matched literally.
func Test_ccomplete_no_exec_via_typeref_bracket()
CheckUnix
let sentinel = tempname()
call delete(sentinel)
let tagsfile = s:WriteTags([
\ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:struct:[|call system('touch " .. sentinel .. "')|####",
\ ])
let save_tags = &tags
let &tags = tagsfile
new
call ccomplete#Complete(1, '')
call ccomplete#Complete(0, 'myvar.x')
call assert_false(filereadable(sentinel),
\ 'typeref field was executed as an Ex command during omni-completion')
bwipe!
let &tags = save_tags
call delete(sentinel)
endfunc
" A legitimate typeref must still drive struct-member completion: escaping the
" field value must not break the normal path.
func Test_ccomplete_typeref_completion_still_works()
let tagsfile = s:WriteTags([
\ "myvar\tmain.c\t/^x$/;\"\tv\ttyperef:struct:mystruct",
\ "alpha\tmain.c\t/^x$/;\"\tm\tstruct:mystruct",
\ "beta\tmain.c\t/^x$/;\"\tm\tstruct:mystruct",
\ ])
let save_tags = &tags
let &tags = tagsfile
new
call ccomplete#Complete(1, '')
let items = ccomplete#Complete(0, 'myvar.')
call assert_equal(type([]), type(items),
\ 'ccomplete#Complete did not return a list')
let names = map(copy(items), 'v:val.word')
call assert_true(index(names, 'alpha') >= 0,
\ 'struct member "alpha" missing from completion: ' . string(names))
call assert_true(index(names, 'beta') >= 0,
\ 'struct member "beta" missing from completion: ' . string(names))
bwipe!
let &tags = save_tags
endfunc
" vim: shiftwidth=2 sts=2 expandtab