The ! shell-escape command in less (command.c:322 A_SHELL branch) processes the user-typed string with fexpand(), then hands the result to lsystem():
case A_SHELL: {
...
if (*cbuf != '!')
{
if (shellcmd != NULL)
free(shellcmd);
shellcmd = fexpand(cbuf); /* (1) expands % and # */
}
...
lsystem(shellcmd, done_msg);
break;
}
fexpand() substitutes % with the current filename and # with the previous filename, using xcpy_filename():
static void xcpy_filename(xcpy *xp, constant char *str)
{
/* If filename contains spaces, quote it
* to prevent edit_list from splitting it. */
lbool quote = (strchr(str, ' ') != NULL);
if (quote)
xcpy_char(xp, openquote);
for (; *str != '\0'; str++)
xcpy_char(xp, *str);
if (quote)
xcpy_char(xp, closequote);
}
The function quotes only when the filename contains a space. Filenames containing other shell metacharacters (;, |, &, $, <, >, backticks, (, ), etc.) but no space pass through verbatim. When lsystem() then hands the assembled command to $SHELL -c, the inner shell parses these as metacharacters.
lsystem()'s call to shell_quote(cmd) protects only the outer-shell level — the inner shell sees the original cmd and parses its metachars normally.
Reproduction
$ mkdir -p /tmp/metafile_test
$ echo 'innocent' > '/tmp/metafile_test/foo;rm'
$ less '/tmp/metafile_test/foo;rm'
:!cat %
cat foo;rm is what reaches the inner shell — the ; is unquoted because the filename contains no space.
In a more realistic attack model the user opens a directory of files (e.g. a cloned repo) containing one with shell metacharacters in its name, then uses !cmd % against it. Filenames with metacharacters but no spaces are uncommon but achievable in attacker-controlled directories.
I think using shell_quote() rather than the space-only check is safer, similar to what is done with the %g prompt protochar in prompt.c:342 which already uses shell_quote() correctly for the v (visual) editor path; aligning xcpy_filename with that approach removes the asymmetry.
Of course, if the user uses single quotes around the CMD command the problem is avoided.
The
!shell-escape command in less (command.c:322A_SHELLbranch) processes the user-typed string withfexpand(), then hands the result tolsystem():fexpand()substitutes%with the current filename and#with the previous filename, usingxcpy_filename():The function quotes only when the filename contains a space. Filenames containing other shell metacharacters (
;,|,&,$,<,>, backticks,(,), etc.) but no space pass through verbatim. Whenlsystem()then hands the assembled command to$SHELL -c, the inner shell parses these as metacharacters.lsystem()'s call toshell_quote(cmd)protects only the outer-shell level — the inner shell sees the original cmd and parses its metachars normally.Reproduction
cat foo;rmis what reaches the inner shell — the;is unquoted because the filename contains no space.In a more realistic attack model the user opens a directory of files (e.g. a cloned repo) containing one with shell metacharacters in its name, then uses
!cmd %against it. Filenames with metacharacters but no spaces are uncommon but achievable in attacker-controlled directories.I think using
shell_quote()rather than the space-only check is safer, similar to what is done with the%gprompt protochar inprompt.c:342which already usesshell_quote()correctly for thev(visual) editor path; aligningxcpy_filenamewith that approach removes the asymmetry.Of course, if the user uses single quotes around the CMD command the problem is avoided.