Skip to content

security: harden scheduler/auth/mcp against shell injection (port upstream #32) - #41

Merged
JeanBaptisteRenard merged 1 commit into
mainfrom
security/port-upstream-32-shell-injection
Jun 9, 2026
Merged

security: harden scheduler/auth/mcp against shell injection (port upstream #32)#41
JeanBaptisteRenard merged 1 commit into
mainfrom
security/port-upstream-32-shell-injection

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown
Collaborator

Port of doctly/switchboard#32 (author: @joeytwiddle), manually adapted — the fork's main.js is ~1900 LOC vs upstream's ~350, so this is a full hand-port, not a cherry-pick.

Vectors fixed

1. Scheduler shell injection (high severity)

buildScheduleCommand previously built a shell string by interpolating YAML frontmatter fields (model, permission-mode, max-budget-usd, allowed-tools, append-system-prompt, add-dirs) directly into a bash -c command. A malicious schedule-*.md in any indexed project could achieve RCE within 60 s of the next cron tick.

Fix: buildScheduleCommand now returns { claudeArgs: string[] } — a plain argv array. runScheduleCommand in main.js re-serialises it via quoteArgvForShell() (new helper in shell-profiles.js) before passing to the user's login shell. Each token is POSIX/PowerShell/cmd-quoted so shell metacharacters inside frontmatter values are never interpreted. Input validation (isSafeScalar/assertSafe) additionally rejects control characters, and max-budget-usd is validated as a number.

2. claude-auth.js keychain read

execSync with a shell-interpolated string → execFileSync with an argv array. Prevents $USER or a crafted CLAUDE_CONFIG_DIR from being interpolated into a shell command.

3. mcp-bridge.js lockfile permissions

The lockfile at ~/.claude/ide/<port>.lock contained the MCP auth token and was previously created with default umask (world-readable on many systems). Now written with mode: 0o600. A chmodSync call also tightens permissions on pre-existing files from older builds.

Deliberately deferred

The open-terminal argv refactor (ipcMain.handle('open-terminal', ...)) is a heavier change and is being ported in a separate PR to avoid collision with a parallel in-progress port.

Tests

test/schedule-injection.test.js — 16 new tests covering:

  • argv-array return type
  • hostile payloads (curl, whoami, backticks, $()) survive as literal tokens
  • max-budget-usd numeric validation
  • control-character rejection
  • quoteArgForShell correctness for bash/zsh/PowerShell
  • Full end-to-end: malicious frontmatter → shell command, verify no metacharacters leak outside quoted tokens

All 16 pass. Existing pure-logic tests (32 tests) continue to pass.

Port of doctly#32 (by @joeytwiddle), manually adapted to
the fork's diverged main.js (~1900 LOC vs upstream ~350).

Vectors fixed:
- schedule-runner.js: buildScheduleCommand now returns { claudeArgs }
  (a plain string[]) instead of a shell-interpolated string. Callers
  pass the argv to runScheduleCommand, which re-serialises safely via
  quoteArgvForShell() before handing to the user's login shell. Adds
  isSafeScalar/assertSafe guards that reject control characters and
  validates max-budget-usd is numeric.
- main.js: runScheduleCommand signature changed to accept claudeArgv[],
  imports quoteArgvForShell from shell-profiles.
- schedule-ipc.js: updated call site to use { claudeArgs } destructuring.
- shell-profiles.js: adds quoteArgForShell() and quoteArgvForShell()
  with POSIX/PowerShell/cmd quoting, exported for use by main.js.
- claude-auth.js: keychain read switched from execSync (shell) to
  execFileSync (argv), preventing $USER injection.
- mcp-bridge.js: lockfile written with mode 0o600 (was 0o644); adds
  chmodSync for pre-existing files from older builds.

open-terminal argv refactor is deliberately deferred to a separate PR
to avoid scope collision with a parallel port in progress.
@JeanBaptisteRenard
JeanBaptisteRenard merged commit 454bee7 into main Jun 9, 2026
7 checks passed
@JeanBaptisteRenard
JeanBaptisteRenard deleted the security/port-upstream-32-shell-injection branch June 9, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant