Skip to content

Commit 7ab76a8

Browse files
committed
patch 9.2.0316: [security]: command injection in netbeans interface via defineAnnoType
Problem: [security]: The netbeans defineAnnoType command passes typeName, fg and bg unsanitized to coloncmd(), allowing a malicious server to inject arbitrary Ex commands via '|'. Similarly, specialKeys does not validate key tokens before building a map command. Solution: Validate typeName, fg and bg against an allowlist of safe characters before passing them to coloncmd() Github Advisory: GHSA-mr87-rhgv-7pw6 Supported by AI Signed-off-by: Christian Brabandt <[email protected]>
1 parent 794c304 commit 7ab76a8

8 files changed

Lines changed: 99 additions & 10 deletions

File tree

runtime/doc/netbeans.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*netbeans.txt* For Vim version 9.2. Last change: 2026 Feb 14
1+
*netbeans.txt* For Vim version 9.2. Last change: 2026 Mar 07
22

33

44
VIM REFERENCE MANUAL by Gordon Prieur et al.
@@ -849,7 +849,7 @@ REJECT Not used.
849849
These errors occur when a message violates the protocol:
850850
*E627* *E628* *E629* *E632* *E633* *E634* *E635* *E636*
851851
*E637* *E638* *E639* *E640* *E641* *E642* *E643* *E644* *E645* *E646*
852-
*E647* *E648* *E650* *E651* *E652*
852+
*E647* *E648* *E649* *E650* *E651* *E652*
853853

854854

855855
==============================================================================

runtime/doc/tags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5298,6 +5298,7 @@ E645 netbeans.txt /*E645*
52985298
E646 netbeans.txt /*E646*
52995299
E647 netbeans.txt /*E647*
53005300
E648 netbeans.txt /*E648*
5301+
E649 netbeans.txt /*E649*
53015302
E65 pattern.txt /*E65*
53025303
E650 netbeans.txt /*E650*
53035304
E651 netbeans.txt /*E651*

src/errors.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1664,7 +1664,8 @@ EXTERN char e_invalid_buffer_identifier_in_setdot[]
16641664
INIT(= N_("E647: Invalid buffer identifier in setDot"));
16651665
EXTERN char e_invalid_buffer_identifier_in_close[]
16661666
INIT(= N_("E648: Invalid buffer identifier in close"));
1667-
// E649 unused
1667+
EXTERN char e_invalid_identifier_in_defineannotype[]
1668+
INIT(= N_("E649: Invalid identifier name in defineAnnoType"));
16681669
EXTERN char e_invalid_buffer_identifier_in_defineannotype[]
16691670
INIT(= N_("E650: Invalid buffer identifier in defineAnnoType"));
16701671
EXTERN char e_invalid_buffer_identifier_in_addanno[]

src/netbeans.c

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
#define GUARDEDOFFSET 1000000 // base for "guarded" sign id's
4141
#define MAX_COLOR_LENGTH 32 // max length of color name in defineAnnoType
4242

43+
// Characters valid in a sign/highlight group name
44+
#define VALID_CHARS (char_u *)"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
45+
#define VALID_SIGNNAME_CHARS VALID_CHARS "_"
46+
#define VALID_COLOR_CHARS VALID_CHARS "#"
47+
4348
// The first implementation (working only with Netbeans) returned "1.1". The
4449
// protocol implemented here also supports A-A-P.
4550
static char *ExtEdProtocolVersion = "2.5";
@@ -77,6 +82,22 @@ static int dosetvisible = FALSE;
7782
static int needupdate = 0;
7883
static int inAtomic = 0;
7984

85+
/*
86+
* Return TRUE if "str" contains only characters from "allowed".
87+
* Used to validate NetBeans-supplied strings before interpolating them
88+
* into Ex commands via coloncmd().
89+
*/
90+
static int
91+
nb_is_safe_string(char_u *str, char_u *allowed)
92+
{
93+
if (str == NULL)
94+
return FALSE;
95+
for (char_u *p = str; *p != NUL; p++)
96+
if (vim_strchr(allowed, *p) == NULL)
97+
return FALSE;
98+
return TRUE;
99+
}
100+
80101
/*
81102
* Callback invoked when the channel is closed.
82103
*/
@@ -1949,6 +1970,15 @@ nb_do_cmd(
19491970
VIM_CLEAR(typeName);
19501971
parse_error = TRUE;
19511972
}
1973+
else if (!nb_is_safe_string(typeName, VALID_SIGNNAME_CHARS) ||
1974+
(*fg != NUL && !nb_is_safe_string(fg, VALID_COLOR_CHARS)) ||
1975+
(*bg != NUL && !nb_is_safe_string(bg, VALID_COLOR_CHARS)))
1976+
{
1977+
nbdebug((" invalid chars in typeName/fg/bg in defineAnnoType\n"));
1978+
emsg(_(e_invalid_identifier_in_defineannotype));
1979+
VIM_CLEAR(typeName);
1980+
parse_error = TRUE;
1981+
}
19521982
else if (typeName != NULL && tooltip != NULL && glyphFile != NULL)
19531983
addsigntype(buf, typeNum, typeName, tooltip, glyphFile, fg, bg);
19541984

@@ -2321,10 +2351,24 @@ special_keys(char_u *args)
23212351

23222352
if (strlen(tok) + i < KEYBUFLEN)
23232353
{
2324-
vim_strncpy((char_u *)&keybuf[i], (char_u *)tok, KEYBUFLEN - i - 1);
2325-
vim_snprintf(cmdbuf, sizeof(cmdbuf),
2326-
"<silent><%s> :nbkey %s<CR>", keybuf, keybuf);
2327-
do_map(MAPTYPE_MAP, (char_u *)cmdbuf, MODE_NORMAL, FALSE);
2354+
// Only allow alphanumeric and function-key name characters.
2355+
// Reject anything else to prevent map command injection.
2356+
int safe = TRUE;
2357+
for (char_u *tp = (char_u *)tok; *tp != NUL; tp++)
2358+
{
2359+
if (!ASCII_ISALNUM(*tp) && *tp != '-')
2360+
{
2361+
safe = FALSE;
2362+
break;
2363+
}
2364+
}
2365+
if (safe)
2366+
{
2367+
vim_strncpy((char_u *)&keybuf[i], (char_u *)tok, KEYBUFLEN - i - 1);
2368+
vim_snprintf(cmdbuf, sizeof(cmdbuf),
2369+
"<silent><%s> :nbkey %s<CR>", keybuf, keybuf);
2370+
do_map(MAPTYPE_MAP, (char_u *)cmdbuf, MODE_NORMAL, FALSE);
2371+
}
23282372
}
23292373
tok = strtok(NULL, " ");
23302374
}

src/po/vim.pot

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/testdir/test_netbeans.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ def process_msgs(self, msgbuf):
113113
'endAtomic_Test' : '0:endAtomic!95\n',
114114
'AnnoScale_Test' : "".join(['2:defineAnnoType!60 ' + str(i) + ' "s' + str(i) + '" "x" "=>" blue none\n' for i in range(2, 26)]),
115115
'detach_Test' : '2:close!96\n1:close!97\nDETACH\n',
116-
'specialKeys_overflow_Test' : '0:specialKeys!200 "' + 'A'*80 + '-X"\n'
117-
116+
'specialKeys_overflow_Test' : '0:specialKeys!200 "' + 'A'*80 + '-X"\n',
117+
'defineAnnoType_injection_Test': '1:defineAnnoType!1 "MySign guifg=red|call writefile([\'inject\'],\'Xinject\')|" "tooltip" "glyphFile" 1 2\n'
118118
}
119119
# execute the specified test
120120
if cmd not in testmap:

src/testdir/test_netbeans.vim

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,4 +1024,42 @@ func Test_nb_specialKeys_overflow()
10241024
call s:run_server('Nb_specialKeys_overflow')
10251025
endfunc
10261026

1027+
func Nb_defineAnnoType_injection(port)
1028+
call writefile([], "Xnetbeans", 'D')
1029+
let g:last = 0
1030+
1031+
exe 'nbstart :localhost:' .. a:port .. ':bunny'
1032+
call assert_true(has("netbeans_enabled"))
1033+
call WaitFor('len(ReadXnetbeans()) > (g:last + 2)')
1034+
let g:last += 3
1035+
1036+
split Xcmdbuf
1037+
let cmdbufnr = bufnr()
1038+
call WaitFor('len(ReadXnetbeans()) > (g:last + 2)')
1039+
let g:last += 3
1040+
hide
1041+
1042+
sleep 1m
1043+
1044+
call delete('Xinject')
1045+
call appendbufline(cmdbufnr, '$', 'defineAnnoType_injection_Test')
1046+
" E475 from :sign is expected — catch it before RunServer sees it.
1047+
" give it a bit of time to process it
1048+
try
1049+
sleep 500m
1050+
catch /E475/
1051+
catch /E649/
1052+
endtry
1053+
1054+
" Injected call must not have created this file
1055+
call assert_false(filereadable('Xinject'))
1056+
call delete('Xinject')
1057+
bwipe! Xcmdbuf
1058+
nbclose
1059+
endfunc
1060+
1061+
func Test_nb_defineAnnoType_injection()
1062+
call ch_log('Test_nb_defineAnnoType_injection')
1063+
call s:run_server('Nb_defineAnnoType_injection')
1064+
endfunc
10271065
" vim: shiftwidth=2 sts=2 expandtab

src/version.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,8 @@ static char *(features[]) =
734734

735735
static int included_patches[] =
736736
{ /* Add new patch number below this line */
737+
/**/
738+
316,
737739
/**/
738740
315,
739741
/**/

0 commit comments

Comments
 (0)