Skip to content

security: pass cwd to git via execFileSync, not interpolation through /bin/sh#1368

Open
garagon wants to merge 1 commit intogarrytan:mainfrom
garagon:garagon/security/memory-ingest-cwd-injection
Open

security: pass cwd to git via execFileSync, not interpolation through /bin/sh#1368
garagon wants to merge 1 commit intogarrytan:mainfrom
garagon:garagon/security/memory-ingest-cwd-injection

Conversation

@garagon
Copy link
Copy Markdown
Contributor

@garagon garagon commented May 8, 2026

Summary

bin/gstack-memory-ingest.ts:632-643 ran:

execSync(`git -C ${JSON.stringify(cwd)} remote get-url origin 2>/dev/null`, ...)

JSON.stringify escapes " and \ but not $ or backticks, so a cwd of "$(touch /tmp/marker)" survives JSON quoting and detonates under /bin/sh's command-substitution-inside-double-quotes.

cwd originates from transcript JSONL records under ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl and ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl. The walker grabs the first .cwd it sees per session — agent transcripts are an explicit untrusted surface in the gstack threat model, which is what the L1-L6 sidebar security stack exists for.

Two pivots above the local same-uid bar:

  • Prompt-injection-to-shell. A single line appended to the active session log via prompt injection turns the next /sync-gbrain into RCE under the user's uid (HOME, ssh keys, ngrok creds, gbrain bearers, etc.).
  • Cross-machine transcript share. A colleague's .claude/projects snippet untar'd into a peer's HOME (a documented gbrain dogfooding pattern) → RCE on first sync.

gstack-memory-ingest --incremental is stage 2 of gstack-gbrain-sync.ts:runMemoryIngest (lines 407-433), so any /sync-gbrain run triggers it.

Fix

Swap the one execSync for execFileSync("git", ["-C", cwd, "remote", "get-url", "origin"], ...). No shell, argv passed directly to git. The same module already uses execFileSync for gbrainAvailable() (line 755) and gbrainPutPage() (line 816) — this single execSync was the outlier.

Test

New regression test gstack-memory-ingest security: untrusted cwd cannot trigger shell substitution plants a Claude-Code-shaped JSONL with cwd="$(touch <marker>)" and asserts the marker file is not created after --incremental --quiet.

$ bun test test/gstack-memory-ingest.test.ts
 18 pass
 0 fail
 53 expect() calls

Negative control: stash the patch, run the same test, it fails (marker file gets created on disk). Restore the patch, it passes.

Test plan

  • bun test test/gstack-memory-ingest.test.ts green (18/18)
  • Negative control: revert fix, security test fails with the marker file created (verified)
  • Existing tests unaffected (every other test passes both before and after the fix)

… /bin/sh

`bin/gstack-memory-ingest.ts:632-643` ran `execSync(\`git -C ${JSON.stringify(cwd)}
remote get-url origin 2>/dev/null\`, ...)`. JSON.stringify escapes `"` and `\`
but not `$` or backticks, so a `cwd` of `"$(touch /tmp/marker)"` survived JSON
quoting and detonated under /bin/sh's command-substitution-inside-double-quotes.

`cwd` originates from transcript JSONL records under
`~/.claude/projects/<encoded-cwd>/<uuid>.jsonl` and
`~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. The walker grabs the first
`.cwd` it sees per session. That's an untrusted surface in the gstack threat
model — the L1-L6 sidebar security stack exists exactly because agent
transcripts can carry attacker-influenced text. Two pivots above the local
same-uid bar: (a) prompt-injection appending `cwd="$(...)"` to the active
session log turns the next /sync-gbrain run into RCE under the user's uid;
(b) cross-machine transcript share (a colleague's `.claude/projects` snippet
untar'd into HOME, a documented gbrain dogfooding shape) → RCE on first sync.

Fix swaps the one execSync for `execFileSync("git", ["-C", cwd, "remote",
"get-url", "origin"], ...)`. No shell, argv passed directly to git. The same
module already uses execFileSync for `gbrainAvailable()` (line 762 pre-patch)
and `gbrainPutPage()` (line 816 pre-patch) — this single execSync was the
outlier.

Test: `gstack-memory-ingest security: untrusted cwd cannot trigger shell
substitution` plants a Claude-Code-shaped JSONL with cwd=`$(touch <marker>)`
and asserts the marker file is not created after `--incremental --quiet`.
Negative control: with the patch reverted, the test fails (marker created);
with the patch applied, it passes (18/18 in test/gstack-memory-ingest.test.ts).
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