Skip to content

fix(cli/mcp): prevent command injection via $EDITOR/$VISUAL#5288

Merged
SivanCola merged 2 commits into
esengine:main-v2from
YoungDan-hero:dev-6
Jun 27, 2026
Merged

fix(cli/mcp): prevent command injection via $EDITOR/$VISUAL#5288
SivanCola merged 2 commits into
esengine:main-v2from
YoungDan-hero:dev-6

Conversation

@YoungDan-hero

@YoungDan-hero YoungDan-hero commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

mcpEditConfigLaunchCommand concatenated the raw $VISUAL/$EDITOR value
into a sh -lc "<editor> <path>" command string. Only the path was
shell-quoted, so an editor value like vim; rm -rf ~ (or any environment
pollution) would execute arbitrary shell commands.

Severity

Command-injection hardening for local environment-controlled editor values.
Exploitation requires the environment variable to be set to a malicious value;
this is not remote-input execution, but it is still a real local attack surface
for shared accounts, compromised tooling, or untrusted config sources. Removing
the shell interpretation layer makes the MCP config editor launch path safer.

Fix

Replace the sh -lc construction with editorLaunchCmd:

  1. Expand $VAR references via os.ExpandEnv (no shell)
  2. Parse the editor value into argv with shell-style quote/backslash handling
    for word splitting only (supports code --wait, emacsclient -a '', and
    quoted editor paths)
  3. Expand leading ~ / ~/ via os.UserHomeDir (no shell)
  4. exec.Command(args[0], append(args[1:], path)...) -- binary resolved by OS

Shell metacharacters become literal argv tokens and cannot be executed. This
matches the safe pattern already used by the terminal-editor fallback
(exec.Command(bin, path)) in the same function.

Shell operators, globbing, command substitution, and redirection are
intentionally not evaluated. The now-unused shellQuote helper is removed.

Behavior preservation

EDITOR value Prior (sh -lc) New (exec)
vim works works
code --wait works works
emacsclient -c -a '' works works, including the empty argument
$HOME/bin/x shell expands ExpandEnv expands
~/bin/x shell expands leading tilde expands
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' --wait shell parses quoted path argv parser preserves quoted path
vim; rm -rf ~ executes rm safe literal argv, no shell execution

Tests

  • TestMCPEditConfigLaunchUsesVisualBeforeEditor -- direct invocation
  • TestMCPEditConfigLaunchEditorWithArgs -- code --wait arg splitting
  • TestMCPEditConfigLaunchEditorParsesShellStyleQuotes -- shell-style quotes,
    empty quoted args, escaped whitespace, and literal backslashes
  • TestMCPEditConfigLaunchEditorRejectsUnterminatedQuote -- invalid quoted
    editor values fail cleanly
  • TestMCPEditConfigLaunchEditorRejectsShellMetachars -- injection defense
  • TestMCPEditConfigLaunchEditorExpandsEnvVar -- $VAR expansion
  • TestMCPEditConfigLaunchEditorExpandsTilde -- ~ / ~/ expansion
  • TestMCPEditConfigLaunchEditorTildeNotInPayload -- tilde not abused
  • TestMCPEditConfigLaunchFallsBackToTerminalEditor -- fallback unchanged

Verification

  • go test ./internal/cli -run 'TestMCPEditConfigLaunch' -- pass
  • go test ./internal/cli -- pass
  • git diff --check -- pass

Cache-impact

None. This only changes local CLI process construction for the MCP config editor
launch path.

Authorship note

@YoungDan-hero is the primary author of this PR. @SivanCola contributed a
maintainer follow-up that preserves shell-style quoted editor values while
keeping the no-shell launch path.

mcpEditConfigLaunchCommand concatenated the raw $VISUAL/$EDITOR value
into a `sh -lc "<editor> <path>"` command string. Only the path was
shell-quoted, so an editor value like `vim; rm -rf ~` (or any
environment pollution) would execute arbitrary shell commands.

Replace the sh -lc construction with editorLaunchCmd, which expands
$VAR references and leading ~/~ prefixes without a shell, splits the
result into argv via strings.Fields, and invokes the binary directly
with exec.Command. This matches the safe pattern already used by the
terminal-editor fallback (exec.Command(bin, path)) in the same function.
Multi-token values such as `code --wait` are still honored.

Add tests covering: direct invocation (no shell), argument splitting,
$VAR expansion, tilde expansion, rejection of shell metacharacters,
and proof that tilde expansion cannot be abused by an injection payload.
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development tui Terminal UI / CLI (internal/cli, internal/control) labels Jun 25, 2026
Parse EDITOR/VISUAL values into argv with shell-style quoting and backslash escaping while still avoiding shell execution. This preserves common editor commands such as emacsclient -a '' and quoted editor paths without reintroducing command injection risk.

Co-authored-by: SivanCola <32437197+SivanCola@users.noreply.github.com>
@SivanCola SivanCola merged commit e2b8f08 into esengine:main-v2 Jun 27, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tui Terminal UI / CLI (internal/cli, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants