From 6b611b0d15603c52ebdad17172b0232b4f65704e 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 +++++++++++++++++++++++++++ src/version.c | 2 + 4 files changed, 67 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 51237be98b..dc3388b524 100644 --- a/runtime/autoload/ccomplete.vim +++ b/runtime/autoload/ccomplete.vim @@ -600,7 +600,7 @@ def StructMembers( # {{{1 return [] endif 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 b81e7e6bf6..dd0708c5ca 100644 --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -243,6 +243,7 @@ NEW_TESTS = \ test_partial \ test_paste \ test_perl \ + test_plugin_ccomplete \ test_plugin_comment \ test_plugin_glvs \ test_plugin_helpcurwin \ @@ -523,6 +524,7 @@ NEW_TESTS_RES = \ test_partial.res \ test_paste.res \ test_perl.res \ + test_plugin_ccomplete.res \ test_plugin_comment.res \ test_plugin_glvs.res \ test_plugin_helpcurwin.res \ diff --git a/src/testdir/test_plugin_ccomplete.vim b/src/testdir/test_plugin_ccomplete.vim new file mode 100644 index 0000000000..a635bd50bd --- /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 diff --git a/src/version.c b/src/version.c index 85bb6d17e1..5a976ac577 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 735, /**/ 734, /**/ -- 2.47.3