Files
msd-core/sdk/src/query
Tom Boucher 4e7e83bf58 fix(3587)(security): argv-based subprocess for check.ship-ready
`gsd-sdk query check.ship-ready <phase>` built a git command as a shell
string with the current branch name interpolated. Git branch names can
legally contain shell metacharacters, so a repo checked out on a
malicious branch like `foo;touch${IFS}INJ;bar` executed arbitrary shell
commands.

Vulnerability site (pre-fix):

  sdk/src/query/check-ship-ready.ts:50
    runSyncSafe(`git config --get branch.${current_branch}.merge`, cwd)
  → execSync('git config --get branch.foo;touch${IFS}INJ;bar.merge')
  → /bin/sh -c parses three commands; the middle one runs `touch INJ`
    in the project dir and creates the sentinel file.

Manually reproduced on git 2.53.0:
  - refname `foo;touch${IFS}INJ;bar` is accepted by `git check-ref-format`
    and by `git checkout -b`.
  - `current_branch` returned from `git rev-parse --abbrev-ref HEAD`
    contains the metacharacters verbatim.
  - Interpolation into the buggy execSync call creates the sentinel.

Fix:

- Replace `runSyncSafe(cmd: string, cwd)` (execSync, shell-string) with
  `runArgvSafe(file, args: readonly string[], cwd)` (execFileSync,
  argv-based, no shell).
- Same shape for the boolean wrapper: `boolArgvSafe`.
- Convert all 7 subprocess sites in the module to argv form:
  - `git status --porcelain`
  - `git rev-parse --abbrev-ref HEAD`
  - `git config --get branch.<name>.merge`   ← the interpolation site
  - `git rev-parse --verify main`
  - `git remote`
  - `gh --version`
  - `which gh`
- Shell is never invoked. Branch names — even ones with `;`, `$IFS`,
  backticks, `$()` — are passed as a single argv element and treated
  as opaque data.

Regression test (`sdk/src/query/check-ship-ready.test.ts`):

- `#3587: branch name with shell-injection payload does not execute
  injected command` — creates a real git repo, checks out the proven
  exploit branch `foo;touch${IFS}INJECTED_BY_3587;bar`, runs
  checkShipReady, and asserts the sentinel file does NOT exist. This
  test FAILS on the unfixed code (verified pre-implementation) and
  PASSES on the fixed code — true red→green TDD.
- `#3587: round-trips a metacharacter branch name verbatim in
  current_branch` — positive proof the branch name survives argv as
  data (would fail if a future change re-introduces shell quoting).
- `#3587: gh probe does not invoke a shell` — locks the gh path
  against a future regression that might add an interpolation site.

Validation:
- SDK unit suite via vitest: 1,869/1,869 pass.
- Full root suite via gsd-test-both (per CLAUDE.md): 10,676/10,676
  on Mac AND 10,676/10,676 on Linux Docker, zero cross-platform diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:42:52 -04:00
..