Skip to content

Ex Command Injection via Unescaped '|' in tagfiles() Path (ccomplete.vim StructMembers)

Moderate
chrisbra published GHSA-r77m-8m55-rpr6 Aug 25, 2026

Package

Vim (Vim)

Affected versions

Vim < 9.2.1011

Patched versions

v9.2.1011

Description

Arbitrary Ex Command Execution via File Names in C Omni-Completion in Vim < 9.2.1011

Date: 25.08.2026
Severity: Medium
CVE: requested, not yet assigned
CWE: Improper Control of Generation of Code ('Code Injection') (CWE-94),
Inclusion of Functionality from Untrusted Control Sphere (CWE-829),
Improper Neutralization of Argument Delimiters in a Command ('Argument Injection') (CWE-88)

Summary

The C omni-completion script in runtime/autoload/ccomplete.vim looks up
struct members by building a :vimgrep command and running it with
:execute. Besides the type name, that command line also holds the list of
tags file names returned by tagfiles(). Those names were escaped for the
space, the backslash, # and %, but not for the bar. Since :vimgrep
accepts another command after a bar, a file name whose name contains one
ends the :vimgrep command early and what follows is executed as an Ex
command when the user invokes omni-completion on a member access.

This is the same defect in the same command line as GHSA-cx73-phcg-3j5g,
fixed in patch v9.2.0845, which addressed only the type name.
Unlike that issue, no crafted tags file content is needed here: the file name
itself carries the payload.

Description

runtime/ftplugin/c.vim sets omnifunc=ccomplete#Complete on C buffers
when filetype plugins are enabled. When completing a member access, and the
declaration is not found in the buffer itself, StructMembers() searches the
tags files:

var fnames: string = tagfiles()
  ->map((_, v: string) => escape(v, ' \#%'))
  ->join()
...
execute 'silent! keepjumps noautocmd '
  .. n .. 'vimgrep ' .. '/\t\V' .. escape(typename, '/\') .. '\m\(\t\|$\)/j '
  .. fnames

tagfiles() returns the names of the tags files in effect for the buffer,
derived from the 'tags' option. The names are appended unquoted to the
:vimgrep argument list.

The :vimgrep command is defined with the EX_TRLBAR flag, so a bar ends
the command and starts a new one. A bar in a file name is therefore not
part of the name but a command separator, and the remainder of the line is
parsed and executed as an independent Ex command by the same :execute.
Escaping the space limits what such a command can contain, but does not
prevent it, and ccomplete.vim is a vim9script file, so the injected text
is parsed with Vim9 syntax. The leading :silent! suppresses the resulting
error, so the injected command runs without a visible failure.

The issue has been addressed by escaping the bar in the tags file names as
well.

Impact

Arbitrary Ex command execution, and through commands such as :! arbitrary
operating-system command execution, in the context of the user running Vim.
Exploitation requires:

  • Vim with filetype plugins enabled
  • a tags file whose path contains a bar, for example because it is stored in
    a directory whose name contains one, and a 'tags' value under which that
    file is found,
  • the victim opening a C file from that tree and invoking omni-completion
    with CTRL-X CTRL-O on a member access whose type is only known from the
    tags file.

The severity is rated Medium. The payload travels in a path rather than in
file content, so it is not visible to review that inspects files only. On
the other hand a bar in a directory name is unusual and conspicuous, it is
not a valid character in file names on MS-Windows, and the crafted name has
no effect until the user deliberately invokes omni-completion on a type that
is not declared in the edited buffer, since the completion would otherwise
not consult the tags files at all.

Acknowledgements

The Vim project would like to thank Yazan Balawneh, Cystack.ps for reporting
the issue.

References

The issue has been fixed as of Vim patch v9.2.1011.

Severity

Moderate

CVE ID

No known CVE

Weaknesses

Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')

The product constructs a string for a command to be executed by a separate component in another control sphere, but it does not properly delimit the intended arguments, options, or switches within that command string. Learn more on MITRE.

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.

Inclusion of Functionality from Untrusted Control Sphere

The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere. Learn more on MITRE.

Credits