Skip to content

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

Open
YoungDan-hero wants to merge 1 commit into
esengine:main-v2from
YoungDan-hero:dev-6
Open

fix(cli/mcp): prevent command injection via $EDITOR/$VISUAL#5288
YoungDan-hero wants to merge 1 commit into
esengine:main-v2from
YoungDan-hero:dev-6

Conversation

@YoungDan-hero

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

P1 (command injection). Exploitation requires the environment variable to be
set to a malicious value — not remote input, but a real attack surface for
shared accounts, compromised tooling, or untrusted config sources.

Fix

Replace the sh -lc construction with editorLaunchCmd:

  1. Expand $VAR references via os.ExpandEnv (no shell)
  2. Split into argv via strings.Fields (supports code --wait)
  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.

The now-unused shellQuote helper is removed.

Behavior preservation

EDITOR value Prior (sh -lc) New (exec)
vim
code --wait ✅ (Fields split)
$HOME/bin/x ✅ (shell expand) ✅ (ExpandEnv)
~/bin/x ✅ (shell expand) ✅ (tilde expand)
~ ✅ (shell expand) ✅ (tilde expand)
vim; rm -rf ~ ❌ executes rm ✅ safe (literal)

No behavior change for legitimate values. Shell quoting inside the editor
value (e.g. 'my editor') is not supported, but was already broken under
the prior sh -lc path for non-single-quoted values, and no common
EDITOR/VISUAL value requires it.

Test (7 cases)

  • TestMCPEditConfigLaunchUsesVisualBeforeEditor — direct invocation
  • TestMCPEditConfigLaunchEditorWithArgscode --wait arg splitting
  • TestMCPEditConfigLaunchEditorRejectsShellMetachars — injection defense
  • TestMCPEditConfigLaunchEditorExpandsEnvVar$VAR expansion
  • TestMCPEditConfigLaunchEditorExpandsTilde~ / ~/ expansion
  • TestMCPEditConfigLaunchEditorTildeNotInPayload — tilde not abused
  • TestMCPEditConfigLaunchFallsBackToTerminalEditor — fallback unchanged

Verification

  • go build ./... — pass
  • go test ./internal/cli/ — all pass
  • go vet ./internal/cli/... — clean
  • gofmt -l — clean

Cache-impact: none

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
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.

1 participant