From 6c3ee4e150433e9a19858e14308ba87560640d3f Mon Sep 17 00:00:00 2001 From: Hirohito Higashi Date: Fri, 26 Jun 2026 15:41:24 +0900 Subject: [PATCH] patch 9.2.0735: [security]: arbitrary Ex command execution during C omni-completion Problem: [security]: With C omni-completion, a crafted tags file can execute arbitrary Ex commands when completing a struct/union member (cipher-creator) Solution: Escape the type field before inserting it into the :vimgrep pattern so it cannot close the pattern and start a new command (Hirohito Higashi). Github Security Advisory: https://github.com/vim/vim/security/advisories/GHSA-mf92-v4xw-j45x Co-Authored-By: Claude Opus 4.8 (1M context) " Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- runtime/autoload/ccomplete.vim | 2 +- src/testdir/Make_all.mak | 2 + src/testdir/test_plugin_ccomplete.vim | 62 +++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/testdir/test_plugin_ccomplete.vim diff --git a/runtime/autoload/ccomplete.vim b/runtime/autoload/ccomplete.vim index 7096dcf4a..fa69f57eb 100644 --- a/runtime/autoload/ccomplete.vim +++ b/runtime/autoload/ccomplete.vim @@ -560,7 +560,7 @@ def StructMembers( # {{{1 if !cached while 1 execute 'silent! keepjumps noautocmd ' - .. n .. 'vimgrep ' .. '/\t' .. typename .. '\(\t\|$\)/j ' + .. n .. 'vimgrep ' .. '/\t' .. escape(typename, '/\') .. '\(\t\|$\)/j ' .. fnames qflist = getqflist() diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak index 66e94c0a2..49c034fb3 100644 --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -227,6 +227,7 @@ NEW_TESTS = \ test_partial \ test_paste \ test_perl \ + test_plugin_ccomplete \ test_plugin_netrw \ test_plugin_python3complete \ test_plugin_tar \ @@ -479,6 +480,7 @@ NEW_TESTS_RES = \ test_partial.res \ test_paste.res \ test_perl.res \ + test_plugin_ccomplete.res \ test_plugin_netrw.res \ test_plugin_python3complete.res \ test_plugin_tar.res \ diff --git a/src/testdir/test_plugin_ccomplete.vim b/src/testdir/test_plugin_ccomplete.vim new file mode 100644 index 000000000..a635bd50b --- /dev/null +++ b/src/testdir/test_plugin_ccomplete.vim @@ -0,0 +1,62 @@ +" 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 + +" 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